Zenn
🥺

メモリと仲良しになろう![Private Field アクセス編]

2025/03/04に公開
7

誰のための記事か?(for whom)

  • メモリ上でどのようにデータが載るのかが分からない人

なぜ学ぶのか? (why)

  • メモリ上にデータがどのように展開されるかを知り、メモリへの知見を深めるため

はじめに

実はプライベートフィールドにアクセスするという話は各所で擦られまくっている話です。
であるにも関わらず、なぜこの話題を再び掘り起こしたのか。

それは「メモリの基礎を理解するために最も適した遊びだから」です。

この記事を通して、さらなるメモリへの理解へと繋げていただけると幸いです。

1. 前回の記事を軽く振り返る

https://zenn.dev/tomoikey/articles/ab2b065bdf334c

  • メモリとは?
    * コンピュータさんが使う道具を置いておくための作業机
  • アドレスとは?
    * 作業道具を収納する引き出しに付いている番号
  • メモリレイアウトとは?
    * データの間取り

前回の記事ではメモリに関する基礎的な知識について触れました。
データは巨大な作業机の上に載せられること、データが格納されている引き出しには番号シールが貼ってあること、そしてデータがどのように引き出しに収納されるのかが静的に決まっていること、それらが理解できたと思います。

これらの知識は、プライベートフィールドにアクセスする遊びを通して体系的に学ぶことができます。

2. 全てのデータはメモリに載る

私たちがプライベートフィールドにアクセスできないのは「単にプログラミング言語がプライベートフィールドへアクセスする書き方自体を禁止しているから」ただそれだけです。

プライベートであろうがパブリックであろうが関係なく、どんなデータも平等にメモリ上に載るのです。従ってプライベートフィールドであってもアドレスさえ分かればデータを読み書きすることができます

実際にやってみましょう。

3. プライベートフィールドにアクセスしてみよう!

構造体の定義

では題材として下記のような構造体を定義してみましょう。

model/sample.go
package model

// Sample という名前の構造体を定義
type Sample struct {
    a int // Go ではフィールド名を小文字始まりにすることでプライベートフィールドになる
    b string
    c []string
}

// 外部パッケージから、データを持った `Sample` 構造体を初期化するための関数
func NewSample() Sample {
    return Sample{a: 1, b: "hello world!", c: []string{"foo", "bar", "baz"}}
}

Sample という構造体を作成しました。NewSample という関数を通して Sample にデータを載せた状態で初期化できるようにしてます。

Go を知らない人向けにすこし詳しく説明します。まず構造体というのは他の言語でいうクラスのようなもので、データをひとまとまりにするものです。今回例に挙げた Sample 構造体は、それぞれプライベートな「a というフィールド名の整数型」「b というフィールド名の文字列型」「c というフィールド名の文字列配列型」が定義されています。これらは外部のパッケージから読み書きすることはできません。そのため NewSample というコンストラクタのような関数を定義しています。この関数を利用することで外部パッケージからでも Sample 構造体にデータを持たせて初期化できるようになっています。

まず普通にプライベートフィールドにアクセスしてみる

先ほど定義した NewSample を利用して Sample 構造体を初期化してから、プライベートフィールドの a というフィールドにアクセスしてみましょう。

main.go
func main() {
    sample := model.NewSample() // NewSample 関数で初期化して sample という変数に格納
    fmt.Println("%d\n", sample.a) // sample の a を Print する
}

上記のコードを実行すると下記のようになります。

出力
❯ go run main.go
# command-line-arguments
./main.go:12:29: sample.a undefined (cannot refer to unexported field a)

当然ですがコンパイルできませんでした。

何が問題かというと外部パッケージで {Sample 構造体}.a と書くことです。逆に言えばこれ以外のことはなんでも許されます。次に進みましょう。

構造体のアドレスを取得する

Go では unsafe package を import するとアドレスを取得する Pointer 関数を利用することができます。これをアドレスを表現する数値型に変換します。

これで取得したアドレスを表示してみましょう。

main.go
func main() {
    sample := model.NewSample()

    rootAddress := uintptr(unsafe.Pointer(&sample))
	
    fmt.Printf("root address: 0x%x\n", rootAddress)
}
出力
❯ go run main.go
root address: 0x1400009eda8

無事取得できましたね。Go 以外の言語で試したい方も同様にメモリアドレスを取得する方法を調べることから始めてみましょう。

フィールドのアドレスを取得する

プライベートフィールドなので直接アドレスを取得することはできません。ではどうすれば良いのでしょう。構造体のメモリレイアウトが分かれば良さそうです。

ちょうど良いものがあります。Go では reflect という package を利用することで構造体のフィールドのオフセットを取得することができます。オフセットを取得できるということは、構造体のアドレスからプライベートフィールドのアドレスを特定することができますね。

この原理を理解できたのであればもう簡単です。reflect の知識が少々必要ですが、Sample 構造体の型情報を取得して個々のフィールドのオフセットからアドレスを計算しています。

下記のコードを実行してみましょう。

main.go
func main() {
    sample := model.NewSample()

    rootAddress := uintptr(unsafe.Pointer(&sample))

    t := reflect.TypeOf(sample)

    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        offset := field.Offset
        address := rootAddress + offset

        fmt.Printf("field address: 0x%x\n", address)
    }
}
出力
❯ go run main.go
field address: 0x14000078d98
field address: 0x14000078da0
field address: 0x14000078db0

アドレスが取得できましたね!犯人の居場所が特定できたのであればあと一息です。

アドレスにアクセスする

Go で生のアドレスから any 型に変換する方法は下記の通りです。

reflect.NewAt({型情報}, {アドレス}).Elem().Interface()

これを利用すればあとはちょっとコードを付け足すだけで良さそうですね。書き足してみましょう。

main.go
func main() {
    sample := model.NewSample()

    rootAddress := uintptr(unsafe.Pointer(&sample))

    t := reflect.TypeOf(sample)

    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        offset := field.Offset
        address := rootAddress + offset

        // アドレスの生データから any データ型に変換するコードを書き足した
        value := reflect.NewAt(field.Type, unsafe.Pointer(address)).Elem().Interface()

        fmt.Printf("%v\n", value)
    }
}

では実行してみましょう... ドキドキしてきましたね...

出力
❯ go run main.go
1
hello world!
[foo bar baz]

おおおおおお!!我々が学んだ理論通りの結果が返ってきました!なんとプライベートフィールドにアクセスできちゃいました!これでどんなプライベートも筒抜けですね!やったーー!!

「プライベートフィールドにアクセスしてどう開発に役立つのか?」という横槍は気にする必要ありません。なぜなら我々はメモリに対する理解を深めているだけに過ぎないのですから。

まとめ

メモリに対する理解を深めればプライベートフィールドであろうがアクセスすることは簡単だということが分かりました。前回の記事は座学だけだったので少々退屈だったと思いますが、今回はハンズオン形式だったので楽しく学べたことだろうと思います。

プライベートフィールドにアクセスできるのであればもう「メモリとお友達になった」と言えるでしょう。ぜひ周りのみんなに自慢しちゃいましょう。

この辺で筆を止めたいと思います。ご精読ありがとうございました。
よかったら GitHub のフォローをいただけると嬉しいです!では良いメモリライフを!

https://github.com/tomoikey

7

Discussion

ログインするとコメントできます