golang: 利用unsafe操做未導出變量

看了 @喻恆春 大神的利用unsafe.Pointer來突破私有成員,以爲例子舉得不太好。並且不該該簡單的放個demo,至少要講一下其中的原理,讓看的童鞋明白因此然。see:http://my.oschina.net/achun/blog/122540golang

unsafe.Pointer其實就是相似C的void *,在golang中是用於各類指針相互轉換的橋樑。uintptr是golang的內置類型,是能存儲指針的整型,uintptr的底層類型是int,它和unsafe.Pointer可相互轉換。uintptr和unsafe.Pointer的區別就是:unsafe.Pointer只是單純的通用指針類型,用於轉換不一樣類型指針,它不能夠參與指針運算;而uintptr是用於指針運算的,GC 不把 uintptr 當指針,也就是說 uintptr 沒法持有對象,uintptr類型的目標會被回收。golang的unsafe包很強大,基本上不多會去用它。它能夠像C同樣去操做內存,但因爲golang不支持直接進行指針運算,因此用起來稍顯麻煩。shell

切入正題。利用unsafe包,可操做私有變量(在golang中稱爲「未導出變量」,變量名以小寫字母開始),下面是具體例子。佈局

在$GOPATH/src下創建poit包,並在poit下創建子包p,目錄結構以下:測試

$GOPATH/srcui

----poitthis

--------p.net

------------v.go
指針

--------main.gocode

如下是v.go的代碼:對象

package p

import (
    "fmt"
)

type V struct {
    i int32
    j int64
}

func (this V) PutI() {
    fmt.Printf("i=%d\n", this.i)
}

func (this V) PutJ() {
    fmt.Printf("j=%d\n", this.j)
}

意圖很明顯,我是想經過unsafe包來實現對V的成員i和j賦值,而後經過PutI()和PutJ()來打印觀察輸出結果。

如下是main.go源代碼:

package main

import (
    "poit/p"
    "unsafe"
)

func main() {
    var v *p.V = new(p.V)
    var i *int32 = (*int32)(unsafe.Pointer(v))
    *i = int32(98)
    var j *int64 = (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(unsafe.Sizeof(int32(0)))))
    *j = int64(763)
    v.PutI()
    v.PutJ()
}

固然會有些限制,好比須要知道結構體V的成員佈局,要修改的成員大小以及成員的偏移量。咱們的核心思想就是:結構體的成員在內存中的分配是一段連續的內存,結構體中第一個成員的地址就是這個結構體的地址,您也能夠認爲是相對於這個結構體偏移了0。相同的,這個結構體中的任一成員均可以相對於這個結構體的偏移來計算出它在內存中的絕對地址。

具體來說解下main方法的實現:

var v *p.V = new(p.V)

new是golang的內置方法,用來分配一段內存(會按類型的零值來清零),並返回一個指針。因此v就是類型爲p.V的一個指針。

var i *int32 = (*int32)(unsafe.Pointer(v))

將指針v轉成通用指針,再轉成int32指針。這裏就看到了unsafe.Pointer的做用了,您不能直接將v轉成int32類型的指針,那樣將會panic。剛纔說了v的地址其實就是它的第一個成員的地址,因此這個i就很顯然指向了v的成員i,經過給i賦值就至關於給v.i賦值了,可是別忘了i只是個指針,要賦值得解引用。

*i = int32(98)

如今已經成功的改變了v的私有成員i的值,好開心^_^

可是對於v.j來講,怎麼來獲得它在內存中的地址呢?其實咱們能夠獲取它相對於v的偏移量(unsafe.Sizeof能夠爲咱們作這個事),但我上面的代碼並無這樣去實現。各位別急,一步步來。

var j *int64 = (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(unsafe.Sizeof(int32(0)))))

其實咱們已經知道v是有兩個成員的,包括i和j,而且在定義中,i位於j的前面,而i是int32類型,也就是說i佔4個字節。因此j是相對於v偏移了4個字節。您能夠用uintptr(4)或uintptr(unsafe.Sizeof(int32(0)))來作這個事。unsafe.Sizeof方法用來獲得一個值應該佔用多少個字節空間。注意這裏跟C的用法不同,C是直接傳入類型,而golang是傳入值。之因此轉成uintptr類型是由於須要作指針運算。v的地址加上j相對於v的偏移地址,也就獲得了v.j在內存中的絕對地址,別忘了j的類型是int64,因此如今的j就是一個指向v.j的指針,接下來給它賦值:

*j = int64(763)

好吧,如今貌視一切就緒了,來打印下:

v.PutI()
v.PutJ()

若是您看到了正確的輸出,那恭喜您,您作到了!

可是,別忘了上面的代碼實際上是有一些問題的,您發現了嗎?

在p目錄下新建w.go文件,代碼以下:

package p

import (
    "fmt"
    "unsafe"
)

type W struct {
    b byte
    i int32
    j int64
}

func init() {
    var w *W = new(W)
    fmt.Printf("size=%d\n", unsafe.Sizeof(*w))
}

須要修改main.go的代碼嗎?不須要,咱們只是來測試一下。w.go裏定義了一個特殊方法init,它會在導入p包時自動執行,別忘了咱們有在main.go裏導入p包。每一個包均可定義多個init方法,它們會在包被導入時自動執行(在執行main方法前被執行,一般用於初始化工做),可是,最好在一個包中只定義一個init方法,不然您或許會很難預期它的行爲)。咱們來看下它的輸出:

size=16

等等,好像跟咱們想像的不一致。來手動計算一下:b是byte類型,佔1個字節;i是int32類型,佔4個字節;j是int64類型,佔8個字節,1+4+8=13。這是怎麼回事呢?這是由於發生了對齊。在struct中,它的對齊值是它的成員中的最大對齊值。每一個成員類型都有它的對齊值,能夠用unsafe.Alignof方法來計算,好比unsafe.Alignof(w.b)就能夠獲得b在w中的對齊值。同理,咱們能夠計算出w.b的對齊值是1,w.i的對齊值是4,w.j的對齊值也是4。若是您認爲w.j的對齊值是8那就錯了,因此咱們前面的代碼能正確執行(試想一下,若是w.j的對齊值是8,那前面的賦值代碼就有問題了。也就是說前面的賦值中,若是v.j的對齊值是8,那麼v.i跟v.j之間應該有4個字節的填充。因此獲得正確的對齊值是很重要的)。對齊值最小是1,這是由於存儲單元是以字節爲單位。因此b就在w的首地址,而i的對齊值是4,它的存儲地址必須是4的倍數,所以,在b和i的中間有3個填充,同理j也須要對齊,但由於i和j之間不須要填充,因此w的Sizeof值應該是13+3=16。若是要經過unsafe來對w的三個私有成員賦值,b的賦值同前,而i的賦值則須要跳過3個字節,也就是計算偏移量的時候多跳過3個字節,同理j的偏移能夠經過簡單的數學運算就能獲得。

好比也能夠經過unsafe來靈活取值:

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var b []byte = []byte{'a', 'b', 'c'}
    var c *byte = &b[0]
    fmt.Println(*(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(c)) + uintptr(1))))
}

關於填充,FastCGI協議就用到了。

相關文章
相關標籤/搜索