この記事は jsys Advent Calendar 2025 12日目の記事です。11日目はCentraさんによる そぽ祭りには実はサイネージがあった!?!?!?!?です。自分のSNSに記事を書くの面白いなと思いました。
はじめはポエムを書こうと思っていた*1のですが、あまり良いものが思いつかなかったのでパソコンの記事を書くことにしました。
世の中にはC言語の構造体で表されるものが多くあります。その一例としてELFのヘッダがあります。
先日ELFヘッダをGo言語から参照したいことがありました。Go言語には debug/elfという標準ライブラリがあり、これを使うと自分でバイナリをパースすることなくELFの中身を解析することができます。
とても便利ですね。。。と思いきや、このライブラリではサポートされていないものが当然あるのでした。
今回はELFのRelocation Entriesをパースする必要がありました。Relocation Entriesとは動的リンクされた関数などのシンボルを、実行時に再配置が必要な箇所のオフセットと実際にどのシンボルに紐づけるべきかを表す構造体です。
typedef struct { Elf32_Addr r_offset; uint32_t r_info; int32_t r_addend; } Elf32_Rela;
この構造体を読むにあたり、Goの構造体のアラインメントはC言語とは異なる*2ため、直接変換することはできません。
Rustの場合は #repr(C)と書くことでC言語のレイアウトでRustの構造体を表すことができます*3。しかし、Goでは定義した構造体のレイアウトを変更することは難しいです*4。
一方でGo言語にはCGOと呼ばれる、Goのプログラム中にC言語のコードを埋め込み呼び出すことができる仕組みがあります。この仕組みを使い、強引にCの構造体をGo言語で扱うことができるのではないでしょうか。
実際に試してみます。
Goの中に含めるCのプログラムは次のようにコメントで表現します。直後にimport Cと書くとCGOとして扱われC.Elf64_RelaのようにGo言語のプログラム側から見えるようになります。
/* #include <stdint.h> typedef uint64_t Elf64_Addr; typedef int64_t Elf64_Sxword; typedef uint64_t Elf64_Xword; typedef struct { Elf64_Addr r_offset; Elf64_Xword r_info; Elf64_Sxword r_addend; } Elf64_Rela; */ import "C"
実際にファイルを読み込んで試してみましょう。io.Readerで読み取ったbufは[]byte型となります。構造体にするためにはC.Elf64_Rela型をなんとかして[]byte型として扱う必要があります。そのために、次の手順を踏みます。
&C.Elf64_Rela型をunsafe.Pointer型に変換します- unsafe.Pointer型を
*[unsafe.Sizeof(rela)]byte型、つまりbyteのスライスにキャストします - これでbyteのスライス同士にすることができたので、bufをコピーします。
sec := ctx.file.Section(".rela.plt") if sec == nil { return errors.New("No .rela.plt section found") } reader := sec.ReaderAt for offset := int64(0); offset < int64(sec.Size); offset += int64(sec.Entsize) { var rela C.Elf64_Rela buf := make([]byte, sec.Entsize) read, err := reader.ReadAt(buf, offset) if err != nil || read != int(sec.Entsize) { return errors.New("Failed to read .rela.plt entry") } buf = buf[:read] copy((*[unsafe.Sizeof(rela)]byte)(unsafe.Pointer(&rela))[:], buf) // バイトスライスをC構造体にコピー }
いかがでしょうか。私はRustでこのようなことをするのはwasabiOSをやった*5ときに何度もあったのですが、Goでも同じことができることに驚きました。
Go言語は標準ライブラリがが充実しており、課題などで急いでコードを書くときに重宝するのですが、このように標準ライブラリにない構造体を扱うのもできるとなるとかなり活用の幅が広がりそうです。
お読みいただきありがとうございました。
おまけ
今回のようにRelocation Entryを読んだり、PLTを読んだり頑張ると動的リンクされた関数の呼び出しを解析することができます。こちらもかなり大変だったので機会があればご紹介したいですね*6。
$ go run . ./sample
Function table built with 10 entries
Relocation table built with 6 entries
Symbol: frame_dummy, Address: 0x7e0, Instruction Count: 0
Symbol: main, Address: 0x7e8, Instruction Count: 18
-> calls dynamic linked function at offset: 0x0
-> calls dynamic linked function: libc.so.6:puts
-> calls dynamic linked function: libc.so.6:sleep
Symbol: call_weak_fn, Address: 0x6f4, Instruction Count: 5
-> calls dynamic linked function: :__gmon_start__
Symbol: register_tm_clones, Address: 0x750, Instruction Count: 0
Symbol: __do_global_dtors_aux, Address: 0x78c, Instruction Count: 0
Symbol: puts@GLIBC_2.17, Address: 0x0, Instruction Count: 0
Symbol: _fini, Address: 0x830, Instruction Count: 0
Symbol: _start, Address: 0x6c0, Instruction Count: 13
-> calls dynamic linked function: libc.so.6:__libc_start_main
-> calls dynamic linked function: libc.so.6:abort
Symbol: _init, Address: 0x608, Instruction Count: 0
Symbol: deregister_tm_clones, Address: 0x720, Instruction Count: 0