
Dalam catatan ini, kita akan belajar cara mendapatkan kode mesin dari fungsi Go secara langsung dalam runtime, mencetaknya menggunakan disassembler, dan di sepanjang jalan kita akan menemukan beberapa trik seperti mendapatkan alamat fungsi tanpa memanggilnya.
Peringatan : artikel mini ini tidak akan mengajarkan Anda sesuatu yang berguna.
Nilai fungsi di Go
Pertama, mari kita tentukan apa fungsi Go dan mengapa kita membutuhkan konsep nilai fungsi .
Ini paling baik dijelaskan oleh dokumen Panggilan Fungsi Go 1.1 . Dokumen ini bukan baru, tetapi sebagian besar informasi di dalamnya masih relevan.
Pada level terendah, selalu merupakan pointer ke kode yang dapat dieksekusi, tetapi ketika kita menggunakan fungsi / penutupan anonim atau meneruskan fungsi sebagai interface{}
, pointer ini tersembunyi di dalam beberapa struktur.
Nama fungsi itu sendiri bukan ekspresi, oleh karena itu, kode tersebut tidak berfungsi:
compile: cannot take the address of add1
Tetapi pada saat yang sama kita bisa mendapatkan function value
melalui nama fungsi yang sama:
Kode ini diluncurkan, tetapi akan mencetak alamat variabel lokal pada stack, yang tidak persis seperti yang kita inginkan. Tetapi, seperti yang disebutkan di atas, alamat fungsi masih ada, Anda hanya perlu tahu cara mengaksesnya.
Paket reflect
tergantung pada detail implementasi ini untuk berhasil menjalankan reflect.Value.Call()
. Di sana (mencerminkan / makefunc.go) Anda dapat memata-matai langkah selanjutnya untuk mendapatkan alamat fungsi:
dummy := makeFuncStub code := **(**uintptr)(unsafe.Pointer(&dummy))
Kode di atas menunjukkan gagasan dasar bahwa Anda dapat memperbaiki ke suatu fungsi:
add1
fungsi add1
dapat add1
dengan memanggil funcAddr(add1)
.
Mendapatkan blok kode fungsi mesin
Sekarang kita memiliki alamat awal kode mesin fungsi, kami ingin mendapatkan seluruh kode mesin fungsi. Di sini Anda harus dapat menentukan di mana kode fungsi saat ini berakhir.
Jika arsitektur x86 memiliki instruksi panjang tetap, itu tidak akan terlalu sulit dan beberapa heuristik dapat membantu kami, di antaranya:
- Sebagai aturan, pada akhir kode fungsi ada pemukulan dari instruksi
INT3
. Ini adalah penanda yang baik untuk akhir kode fungsi, tetapi mungkin tidak ada. - Fungsi dengan frame non-nol untuk stack memiliki prolog yang memeriksa apakah stack ini perlu diperluas. Jika ya, maka lompatan ke kode dilakukan segera setelah kode fungsi, dan kemudian lompatan ke awal fungsi. Kode yang kami minati akan berada di tengah.
Tetapi Anda harus secara jujur โโmendekode instruksi, karena byte-by-pass dapat menemukan byte INT3
di dalam instruksi lain. Menghitung panjang instruksi untuk dilewati juga tidak mudah, karena itu x86, sayang .
Alamat suatu fungsi dalam konteks paket runtime
kadang-kadang disebut PC
, untuk menekankan kemampuan untuk menggunakan alamat di suatu tempat di dalam fungsi, dan bukan hanya titik masuk dari fungsi. Hasil funcAddr
dapat digunakan sebagai argumen untuk fungsi runtime.FuncForPC()
untuk mendapatkan runtime.Func
tanpa memanggil fungsi itu sendiri. Melalui transformasi Tahun Baru yang tidak aman, kita dapat mengakses runtime._func
, yang informatif, tetapi tidak terlalu berguna: tidak ada informasi tentang ukuran blok kode fungsi.
Tampaknya tanpa bantuan ELF kita tidak bisa mengatasinya.
Untuk platform di mana executable memiliki format berbeda, sebagian besar artikel akan tetap relevan, tetapi Anda harus menggunakan bukan debug/elf
, tetapi paket lain dari debug
.
ELF yang bersembunyi di program Anda
Informasi yang kami butuhkan sudah terkandung dalam metadata file ELF .
Melalui os.Args[0]
kita dapat mengakses file yang dapat dieksekusi itu sendiri, dan sudah mendapatkan tabel simbol darinya.
func readELF() (*elf.File, error) { f, err := os.Open(os.Args[0]) if err != nil { return nil, fmt.Errorf("open argv[0]: %w", err) } return elf.NewFile(f) }
Cari karakter di dalam elf.File
Semua karakter dapat File.Symbols()
menggunakan metode File.Symbols()
. Metode ini mengembalikan []elf.Symbol
, yang berisi bidang Symbol.Size
- ini adalah "ukuran fungsi" yang kita Symbol.Size
. Bidang Symbol.Value
harus cocok dengan nilai yang dikembalikan oleh funcAddr
.
Anda dapat mencari simbol yang diinginkan baik dengan alamat ( Symbol.Value
) atau dengan nama ( Symbol.Name
). Jika karakter diurutkan berdasarkan nama, dimungkinkan untuk menggunakan sort.Search()
, tetapi ini tidak begitu:
Simbol akan dicantumkan sesuai urutan yang muncul dalam file.
Jika Anda sering perlu menemukan karakter dalam tabel, Anda harus membuat indeks tambahan, misalnya, melalui map[string]*elf.Symbol
atau map[uintptr]*elf.Symbol
.
Karena kita sudah tahu cara mendapatkan alamat suatu fungsi berdasarkan nilainya, kita akan mencarinya:
func elfLookup(f *elf.File, value uint64) *elf.Symbol { symbols, err := f.Symbols() if err != nil { return nil } for _, sym := range symbols { if sym.Value == value { return &sym } } return nil }
Catatan : agar pendekatan ini berfungsi, kita membutuhkan tabel karakter. Jika biner dibangun dengan ` -ldflags "-s"
', maka elfLookup()
akan selalu mengembalikan nil
. Jika Anda menjalankan program melalui go run
Anda dapat mengalami masalah yang sama. Untuk contoh dari artikel ini, disarankan untuk melakukan ' go build
' atau ' go install
' untuk mendapatkan file yang dapat dieksekusi.
Mendapatkan kode fungsi mesin
Mengetahui kisaran alamat di mana kode yang dapat dieksekusi terletak, tetap hanya untuk menariknya dalam bentuk []byte
untuk pemrosesan yang mudah.
func funcCode(addr uintptr) ([]byte, error) { elffile, err := readELF() if err != nil { return nil, fmt.Errorf("read elf: %w", err) } sym := elfLookup(elffile, uint64(addr)) if sym == nil { return nil, fmt.Errorf("can't lookup symbol for %x", addr) } code := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ Data: addr, Len: int(sym.Size), Cap: int(sym.Size), })) return code, nil }
Kode ini sengaja disederhanakan untuk demonstrasi. Anda tidak harus membaca ELF
setiap kali dan melakukan pencarian linear di atas meja.
Hasil dari fungsi funcCode()
adalah irisan dengan byte dari kode fungsi mesin. Dia harus funcAddr()
hasil pemanggilan funcAddr()
.
code, err := funcCode(funcAddr(add1)) if err != nil { log.Panicf("can't get function code: %v", err) } fmt.Printf("% x\n", code)
Membongkar kode mesin
Untuk membuat kode mesin lebih mudah dibaca, kami akan menggunakan disassembler.
Saya paling akrab dengan proyek zydis dan Intel XED , jadi pertama-tama pilihan saya jatuh pada mereka.
Untuk Go, Anda dapat mengambil go-zydis binding , yang cukup bagus dan mudah dipasang untuk tugas kami.
Mari kita gambarkan abstraksi "melewati instruksi mesin", dengan bantuan yang memungkinkan untuk mengimplementasikan operasi lain:
func walkDisasm(code []byte, visit func(*zydis.DecodedInstruction) error) error { dec := zydis.NewDecoder(zydis.MachineMode64, zydis.AddressWidth64) buf := code for len(buf) > 0 { instr, err := dec.Decode(buf) if err != nil { return err } if err := visit(instr); err != nil { return err } buf = buf[int(instr.Length):] } return nil }
Fungsi ini mengambil irisan kode mesin sebagai input dan memanggil fungsi callback untuk setiap instruksi yang diterjemahkan.
Berdasarkan itu, kita dapat menulis printDisasm
kita printDisasm
:
func printDisasm(code []byte) error { const ZYDIS_RUNTIME_ADDRESS_NONE = math.MaxUint64 formatter, err := zydis.NewFormatter(zydis.FormatterStyleIntel) if err != nil { return err } return walkDisasm(code, func(instr *zydis.DecodedInstruction) error { s, err := formatter.FormatInstruction(instr, ZYDIS_RUNTIME_ADDRESS_NONE) if err != nil { return err } fmt.Println(s) return nil }) }
Jika kita menjalankan printDisasm
pada add1
fungsi add1
, kita mendapatkan hasil yang sudah lama ditunggu-tunggu:
mov rax, [rsp+0x08] inc rax mov [rsp+0x10], rax ret
Validasi hasil
Sekarang kita akan mencoba memastikan bahwa kode assembler yang diperoleh di bagian sebelumnya sudah benar.
Karena kami sudah memiliki biner terkompilasi, Anda dapat menggunakan objdump
disertakan dengan Go:
$ go tool objdump -s 'add1' exe TEXT main.add1(SB) example.go example.go:15 0x4bb760 488b442408 MOVQ 0x8(SP), AX example.go:15 0x4bb765 48ffc0 INCQ AX example.go:15 0x4bb768 4889442410 MOVQ AX, 0x10(SP) example.go:15 0x4bb76d c3 RET
Semuanya bertemu, hanya sintaks yang sedikit berbeda, yang diharapkan.
Ekspresi metode
Jika kita perlu melakukan hal yang sama dengan metode, maka alih-alih nama fungsi kita akan menggunakan ekspresi metode .
Katakanlah add1
kami add1
bukan fungsi, tetapi metode jenis adder
:
type adder struct{} func (adder) add1(x int) int { return x + 2 }
Maka panggilan untuk mendapatkan alamat fungsi akan terlihat seperti funcAddr(adder.add1)
.
Kesimpulan
Saya sampai pada hal-hal ini bukan karena kebetulan dan, mungkin, dalam salah satu artikel berikut ini saya akan memberi tahu Anda bagaimana rencananya akan menggunakan semua mekanisme ini. Sementara itu, saya sarankan memperlakukan catatan ini sebagai deskripsi dangkal tentang bagaimana runtime
dan reflect
melihat fungsi Go kami melalui nilai fungsi.
Daftar sumber daya yang digunakan: