GoSuda

Analisis Pembangkitan Kode Assembly AMD64 dan Instruksi Percabangan untuk Kondisi (if) pada Go

By Lee Yunjin views ...

If Statement dalam Bahasa Go

Pertama-tama, alasan kami memilih Go adalah karena di antara bahasa modern, Go adalah bahasa yang assembly-nya paling "indah", dan bahkan jika dibandingkan dengan bahasa klasik, efisiensi sintaksnya kadang justru sangat unggul.

Sekarang setelah kita memahami cara kerja program Go sederhana pada pelajaran sebelumnya, mari kita langsung membandingkan Go dan Assembly baris demi baris.

Kode Sumber

Pertama-tama, tidak hanya di Go, bahkan compiler modern termasuk GCC secara otomatis mengoptimasi cabang yang tidak memiliki arti penggunaan. Compiler bahasa C seperti GCC dan Clang juga melakukan optimasi yang sangat agresif pada -O2, yang merupakan standar industri, sehingga era di mana programmer sulit mempercayai compiler sepenuhnya akhirnya sudah berakhir sejak akhir abad ke-20.

Oleh karena itu, agar memiliki makna, kondisi yang diberikan harus sulit diprediksi dan diubah menjadi sintaks lain oleh compiler.

 1package main
 2
 3import (
 4    "os"
 5    "strconv"
 6)
 7
 8func main() {
 9    // Jika ini diisi dengan sesuatu yang dapat diprediksi dan cabangnya dapat dihapus, seperti x = 10,
10    // compiler akan mengoptimasinya dan menghapus cabangnya.
11    // Oleh karena itu, untuk melihat assembly-nya secara langsung, di C digunakan opsi seperti -O0,
12    // atau compiler dibuat menggunakan nilai eksternal yang tidak dapat diprediksi,
13    // tetapi karena bagian ini membahas pemrograman modern, metode mematikan
14    // optimasi biner Go tidak digunakan.
15    if len(os.Args) < 2 {
16        return
17    }
18    x, _ := strconv.Atoi(os.Args[1])
19
20    if x < 10 {
21        println("X is smaller than 10")
22    } else {
23        println("X is larger or same as 10")
24    }
25}

Dalam kasus ini, karena input tidak dapat diprediksi oleh compiler, pernyataan cabang tetap diterjemahkan ke dalam kode mesin apa adanya.

Assembly

 1TEXT main.main(SB) /home/yjlee/introduction-to-golang/learn-golang/if-and-switch/golang-if/main.go
 2  main.go:8             0x47a840                493b6610                CMPQ SP, 0x10(R14)
 3  main.go:8             0x47a844                7670                    JBE 0x47a8b6
 4  main.go:8             0x47a846                55                      PUSHQ BP
 5  main.go:8             0x47a847                4889e5                  MOVQ SP, BP
 6  main.go:8             0x47a84a                4883ec10                SUBQ $0x10, SP
 7  main.go:15            0x47a84e                48833d12fb0a0002        CMPQ os.Args+8(SB), $0x2
 8  main.go:15            0x47a856                7c58                    JL 0x47a8b0
 9  main.go:15            0x47a858                488b0d01fb0a00          MOVQ os.Args(SB), CX
10  main.go:18            0x47a85f                488b4110                MOVQ 0x10(CX), AX
11  main.go:18            0x47a863                488b5918                MOVQ 0x18(CX), BX
12  main.go:18            0x47a867                e834e8ffff              CALL strconv.Atoi(SB)
13  main.go:20            0x47a86c                4883f80a                CMPQ AX, $0xa
14  main.go:20            0x47a870                7d1d                    JGE 0x47a88f
15  main.go:21            0x47a872                e809befbff              CALL runtime.printlock(SB)
16  main.go:21            0x47a877                488d0519f50100          LEAQ 0x1f519(IP), AX
17  main.go:21            0x47a87e                bb15000000              MOVL $0x15, BX
18  main.go:21            0x47a883                e878c6fbff              CALL runtime.printstring(SB)
19  main.go:21            0x47a888                e853befbff              CALL runtime.printunlock(SB)
20  main.go:21            0x47a88d                eb1b                    JMP 0x47a8aa
21  main.go:23            0x47a88f                e8ecbdfbff              CALL runtime.printlock(SB)
22  main.go:23            0x47a894                488d05c8040200          LEAQ 0x204c8(IP), AX
23  main.go:23            0x47a89b                bb1a000000              MOVL $0x1a, BX
24  main.go:23            0x47a8a0                e85bc6fbff              CALL runtime.printstring(SB)
25  main.go:23            0x47a8a5                e836befbff              CALL runtime.printunlock(SB)
26  main.go:25            0x47a8aa                4883c410                ADDQ $0x10, SP
27  main.go:25            0x47a8ae                5d                      POPQ BP
28  main.go:25            0x47a8af                c3                      RET
29  main.go:16            0x47a8b0                4883c410                ADDQ $0x10, SP
30  main.go:16            0x47a8b4                5d                      POPQ BP
31  main.go:16            0x47a8b5                c3                      RET
32  main.go:8             0x47a8b6                e845f0feff              CALL runtime.morestack_noctxt.abi0(SB)
33  main.go:8             0x47a8bb                eb83                    JMP main.main(SB)

Dengan menggunakan go tool, kita dapat mengetahui secara rinci sintaks mana yang berpasangan dengan assembly mana.

Karena yang akan kita pelajari kali ini adalah perbandingan dan pernyataan cabang if, kita cukup memperhatikan beberapa baris saja.

Instruksi CMPQ & Instruksi JL

Instruksi CMPQ adalah instruksi untuk membandingkan tipe data 4 byte (4 word), dan asal katanya dari CoMPareQuadword yang disingkat menjadi CMPQ.

Jika kita melihat alamat memori 0x47a84e, terdapat sintaks CMPQ os.Args+8(SB), $0x2. Dalam kasus ini, program membandingkan jumlah argumen yang diterima dengan heksadesimal 0x2 (yaitu hanya 2).

Setelah itu, untuk memeriksa apakah argumen kurang dari 2 (yaitu jika program itu sendiri adalah satu-satunya argumen), dilakukan lompatan setelah perbandingan melalui JL. Artinya, ini adalah singkatan dari Jump, Less than, sehingga menjadi JL. Jika berdasarkan operasi perbandingan sebelumnya argumen kurang dari 2, program melompat ke alamat 0x47a8b0, dan di sana terdapat JGE. Namun, instruksi ini menggunakan register AX, sehingga kita perlu mengetahui identitas nilai yang tersimpan di register tersebut.

Instruksi MOVQ

Setelah ini, kita perlu memahami cara menyimpan alamat awal data menggunakan register CX secara langsung, serta bagaimana data sebenarnya diekstrak setelah alamat tersebut dibaca.

Pada rentang 0x47858-0x47863, operasi ini dilakukan secara bertahap.

Pertama, alamat awal array argumen dimasukkan ke register CX dengan instruksi MOVQ os.Args(SB), CX. Pada tahap ini, kita harus memahami tipe string di Go.

string di Go adalah struktur, dan struktur ini terdiri dari 2 data berukuran 8 byte, sehingga totalnya 16 byte.

struct8 byte8 byte
stringmem addressstring length

Secara visual, strukturnya seperti di atas: 8 byte pertama menyimpan alamat awal string, dan 8 byte berikutnya menyimpan panjang string.

Oleh karena itu, alamat string disimpan di register AX, dan panjang string disimpan di register BX.

CALL

Pada postingan sebelumnya, ketika kita melihat fungsi-fungsi di kelompok runtime, terdapat instruksi bernama CALL. Instruksi ini dilekatkan pada fungsi-fungsi yang digunakan di Go, dan berarti memanggil suatu fungsi, sehingga memang benar-benar disebut CALL. Setelah itu, fungsi CALL digunakan untuk mengonversi string menjadi bilangan bulat, dan pada tahap ini tidak terlihat dengan jelas di mana bilangan bulat tersebut disimpan karena telah diabstraksikan dalam fungsi.

Instruksi CMPQ & Instruksi JGE

Kembali ke alamat 0x47a86c tadi, instruksi tersebut membandingkan alamat string dengan angka 0xa (desimal 10)!

Artinya, karena argumen tersebut tidak digunakan lagi di dalam program, nilai di lokasi string ditimpa untuk membentuk ruang variabel integer x.

Inilah wujud sebenarnya dari optimasi agresif yang dilakukan pada bahasa seperti Go.

Selanjutnya muncul instruksi JGE, yang merupakan singkatan dari Jump, Greater or Equals. Dengan demikian, instruksi ini memeriksa apakah nilai lebih besar atau sama dengan nilai pembandingnya.

Maka, bukan x < 10 seperti yang tertulis, melainkan arah perbandingan dalam sintaks ini terbalik! Hal ini dilakukan karena dalam kode mesin, melompati lebih awal ketika kondisi tidak terpenuhi lebih intuitif dan menghemat 1 instruksi, dibandingkan dengan melakukan perbandingan satu kali ketika kondisi terpenuhi lalu memeriksa kembali apakah kondisi tidak terpenuhi.

Optimasi semacam ini sangat klasik, sehingga pola ini sering muncul bahkan pada compiler dengan tingkat optimasi yang cukup rendah, berbeda dengan contoh strconv.Atoi yang telah kita lihat sebelumnya, sehingga akan bermanfaat jika Anda memahaminya.

Dengan demikian, dengan menerapkan prinsip ini, kita dapat memperoleh sumber yang berbeda tetapi menghasilkan assembly yang 100% identik.

Contoh Kode Cermin

Dengan menggunakan skrip berikut, skrip bash akan membuat dua sumber yang 100% cermin, lalu memverifikasi bahwa setelah menghilangkan metadata yang berubah-ubah, hanya bagian main yang menghasilkan assembly yang persis sama.

 1#!/usr/bin/env bash
 2
 3# 1. Inisialisasi penuh untuk sisa file dan direktori lama
 4echo "[1/6] Cleaning up old artifacts..."
 5rm -rf test_dir main_orig main_asm orig.asm asm.asm orig_pure.asm asm_pure.asm
 6mkdir -p test_dir
 7
 8# 2. Menulis kode sumber versi asli (main.go)
 9echo "[2/6] Generating main.go..."
10cat << 'EOF' > main.go
11package main
12
13import (
14        "os"
15        "strconv"
16)
17
18func main() {
19        if len(os.Args) < 2 {
20                return
21        }
22        x, _ := strconv.Atoi(os.Args[1])
23
24        s1 := "X is smaller than 10"
25        s2 := "X is larger or same as 10"
26
27        if x < 10 {
28                println(s1)
29        } else {
30                println(s2)
31        }
32}
33EOF
34
35# 3. Menulis kode sumber versi cermin (main_from_asm.go)
36# Struktur operator disinkronkan simetris sempurna agar compiler menggunakan template optimasi (JGE) yang sama
37echo "[3/6] Generating main_from_asm.go..."
38cat << 'EOF' > main_from_asm.go
39package main
40
41import (
42        "os"
43        "strconv"
44)
45
46func main() {
47        if len(os.Args) < 2 {
48                return
49        }
50        x, _ := strconv.Atoi(os.Args[1])
51
52        s1 := "X is smaller than 10"
53        s2 := "X is larger or same as 10"
54
55        // Dengan mempertahankan struktur x < 10 yang berpatokan pada 10, compiler
56        // akan menggunakan mekanisme JGE dan penempatan blok yang persis sama dengan main.go.
57        if x < 10 {
58                println(s1)
59        } else {
60                println(s2)
61        }
62}
63EOF
64
65# 4. Melakukan build di masing-masing sumber dalam direktori dan nama file yang sama
66echo "[4/6] Compiling both sources inside 'test_dir'..."
67cp main.go test_dir/main.go
68cd test_dir && go build -o ../main_orig main.go && cd ..
69
70rm test_dir/main.go
71cp main_from_asm.go test_dir/main.go
72cd test_dir && go build -o ../main_asm main.go && cd ..
73
74# 5. Mengekstrak bagian assembly fungsi main.main murni menggunakan go tool objdump
75echo "[5/6] Extracting main.main assembly sections..."
76go tool objdump -s "main\.main" main_orig > orig.asm
77go tool objdump -s "main\.main" main_asm > asm.asm
78
79# Menyaring hanya field instruksi murni (Opcode & Operands) yang akan dieksekusi CPU,
80# dengan menghapus alamat virtual, offset, dan teks data byte mesin
81awk '{print $4, $5, $6, $7}' orig.asm > orig_pure.asm
82awk '{print $4, $5, $6, $7}' asm.asm > asm_pure.asm
83
84# 6. Verifikasi integritas struktur instruksi mesin melalui diff
85echo "[6/6] Verifying assembly structural integrity via diff..."
86echo "------------------------------------------------------------"
87
88if diff orig_pure.asm asm_pure.asm > /dev/null; then
89    echo "===> [์„ฑ๊ณต] ๋‘ ๋ฐ”์ด๋„ˆ๋ฆฌ์˜ main.main ๊ธฐ๊ณ„์–ด ๋กœ์ง์ด 100% ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค! <==="
90    echo "์ปดํŒŒ์ผ๋Ÿฌ์˜ ์ตœ์ ํ™” ํŒŒ์ดํ”„๋ผ์ธ ๊ฐ€์ด๋“œ๋ผ์ธ์„ ์™„๋ฒฝํ•˜๊ฒŒ ๋™๊ธฐํ™”ํ•˜์—ฌ ๋™์ผํ•œ ์–ด์…ˆ๋ธ”๋ฆฌ๋ฅผ ์–ป์—ˆ์Šต๋‹ˆ๋‹ค."
91else
92    echo "===> [์‹คํŒจ] ์–ด์…ˆ๋ธ”๋ฆฌ ๋ช…๋ น์–ด ๊ตฌ์กฐ์— ์ฐจ์ด์ ์ด ๋ฐœ๊ฒฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. <==="
93    diff -u orig_pure.asm asm_pure.asm
94fi
95echo "------------------------------------------------------------"

Ketika skrip ini benar-benar dijalankan, kita akan memperoleh informasi seperti berikut.

 1[1/6] Cleaning up old artifacts...
 2[2/6] Generating main.go...
 3[3/6] Generating main_from_asm.go...
 4[4/6] Compiling both sources inside 'test_dir'...
 5[5/6] Extracting main.main assembly sections...
 6[6/6] Verifying assembly structural integrity via diff...
 7------------------------------------------------------------
 8===> [์„ฑ๊ณต] ๋‘ ๋ฐ”์ด๋„ˆ๋ฆฌ์˜ main.main ๊ธฐ๊ณ„์–ด ๋กœ์ง์ด 100% ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค! <===
 9์ปดํŒŒ์ผ๋Ÿฌ์˜ ์ตœ์ ํ™” ํŒŒ์ดํ”„๋ผ์ธ ๊ฐ€์ด๋“œ๋ผ์ธ์„ ์™„๋ฒฝํ•˜๊ฒŒ ๋™๊ธฐํ™”ํ•˜์—ฌ ๋™์ผํ•œ ์–ด์…ˆ๋ธ”๋ฆฌ๋ฅผ ์–ป์—ˆ์Šต๋‹ˆ๋‹ค.
10------------------------------------------------------------

Kesimpulan

Bahasa pemrograman menyediakan banyak abstraksi, tetapi di balik abstraksi tersebut terdapat optimasi yang sangat menarik dan agresif. Dengan memanfaatkan hal ini secara terbalik, kita juga dapat membuat kode cermin yang sumbernya berbeda tetapi assembly-nya sama. Jika Anda tertarik pada level rendah dan menemukan perangkat lunak proprietary yang ditulis dengan Go, membongkar assembly-nya secara langsung untuk memulihkan sumbernya tampaknya bukan hal yang mustahil.

Pelajaran Berikutnya

Pada pelajaran berikutnya, kita akan mempelajari pernyataan select-case yang juga menarik, selain If.