Pernyataan If dalam bahasa pemrograman Go
Pernyataan If dalam Bahasa Go
Pertama, alasan kami memilih Go adalah karena di antara bahasa-bahasa modern, Go memiliki assembly yang paling 'estetik', dan efisiensi sintaksisnya sering kali jauh melampaui bahasa-bahasa klasik.
Sekarang, karena pada materi sebelumnya kita telah memahami cara kerja program Go yang sederhana, mari kita langsung membandingkan Go dan Assembly baris demi baris.
Kode Sumber
Pertama, sebagaimana halnya dalam Go, bahkan kompilator modern termasuk GCC secara otomatis mengoptimalkan pernyataan percabangan yang dianggap tidak memiliki signifikansi penggunaan. Kompilator bahasa C seperti GCC dan Clang juga melakukan optimasi yang sangat agresif pada standar industri -O2, sehingga era di mana seorang programmer sulit untuk mempercayai kompilator sepenuhnya sebenarnya telah berakhir sejak akhir abad ke-20.
Oleh karena itu, setidaknya pernyataan tersebut akan memiliki makna jika kita memberikan kondisi yang sulit diprediksi oleh kompilator untuk diubah menjadi sintaksis lain.
1package main
2
3import (
4 "os"
5 "strconv"
6)
7
8func main() {
9 // Jika bagian ini diganti dengan sesuatu yang dapat diprediksi seperti x = 10
10 // yang memungkinkan percabangan dihapus, maka kompilator akan melakukan optimasi
11 // dan menghapus percabangan tersebut.
12 // Oleh karena itu, untuk melihat hal ini secara langsung dalam assembly, dalam bahasa C
13 // kita menggunakan -O0 atau sejenisnya, atau membuat kompilator menggunakan
14 // nilai eksternal yang tidak dapat diprediksi sejak awal. Namun, karena bagian ini
15 // membahas pemrograman modern, kita tidak akan menggunakan metode untuk
16 // mematikan optimasi biner pada Go.
17 if len(os.Args) < 2 {
18 return
19 }
20 x, _ := strconv.Atoi(os.Args[1])
21
22 if x < 10 {
23 println("X is smaller than 10")
24 } else {
25 println("X is larger or same as 10")
26 }
27}
Dalam kasus ini, karena input tidak dapat diprediksi oleh kompilator, pernyataan percabangan tersebut diterjemahkan ke dalam bahasa mesin sebagaimana adanya.
Bahasa 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 diberitahu secara rinci sintaks mana yang cocok dengan assembly yang mana.
Karena yang akan kita pelajari kali ini adalah pernyataan perbandingan dan pernyataan percabangan if, maka ada beberapa baris yang perlu diperhatikan.
Instruksi CMPQ & Instruksi JL
Instruksi CMPQ adalah instruksi untuk membandingkan tipe data 4-byte (4-word), yang berasal dari CoMPareQuadword, kemudian 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 2).
Selanjutnya, jika argumen kurang dari 2 (yaitu jika program hanya memiliki dirinya sendiri sebagai argumen), dilakukan peloncatan (jump) setelah perbandingan melalui JL. Singkatnya, ini adalah Jump, Less than yang disingkat menjadi JL.
Mengenai operasi perbandingan sebelumnya, jika argumen kurang dari 2, program melompat ke alamat 0x47a8b0 di mana terdapat JGE.
Namun, karena sintaks ini menggunakan register AX, kita perlu mengetahui identitas nilai yang tersimpan di dalam register tersebut.
Instruksi MOVQ
Selanjutnya, kita perlu mengetahui bagaimana data sebenarnya diekstraksi setelah membaca alamat dengan menggunakan register 'CX' untuk menyimpan alamat awal data.
Jika kita melihat rentang 0x47858-0x47863, operasi ini dilakukan secara bertahap.
Pertama, alamat awal array argumen dimasukkan ke register CX dengan perintah MOVQ os.Args(SB), CX. Pada tahap ini, kita harus memahami tipe string dalam Go.
string dalam Go adalah sebuah struct, dan struct ini terdiri dari 16 byte yang berasal dari dua data berukuran 8 byte.
| struct | 8 byte | 8 byte |
|---|---|---|
| string | mem address | string length |
Jika digambarkan secara visual, tampilannya seperti di atas, di mana 8 byte pertama menyimpan alamat awal string, dan 8 byte terakhir menyimpan panjang string.
Oleh karena itu, alamat string disimpan di register AX, dan panjang string disimpan di register BX.
CALL
Pada pos sebelumnya, saat kita melihat fungsi-fungsi golongan runtime, terdapat perintah bernama CALL.
Perintah ini disematkan di depan fungsi-fungsi yang digunakan dalam Go, dan secara harfiah berarti CALL, yang artinya memanggil suatu fungsi. Setelah itu, fungsi CALL digunakan untuk mengubah string menjadi integer, namun pada saat itu, di mana integer tersebut disimpan tidak terlihat karena sudah diabstraksikan ke dalam fungsi.
Instruksi CMPQ & Instruksi JGE
Kembali ke alamat tadi, yaitu 0x47a86c, instruksinya sedang membandingkan alamat string dengan angka 0xa (10 dalam desimal)!
Ini berarti, karena program tidak lagi menggunakan argumen tersebut, kompilator menimpa lokasi string untuk membuat tempat bagi variabel integer x.
Inilah wujud nyata dari optimasi agresif yang dilakukan dalam bahasa Go dan lainnya.
Selanjutnya, muncul instruksi JGE, yang merupakan singkatan dari Jump, Greater or Equals. Oleh karena itu, sintaks ini menanyakan apakah nilai tersebut lebih besar atau sama dengan dibandingkan dengan target perbandingan.
Jadi, ini bukan x < 10 sebagaimana sintaks aslinya, melainkan arah perbandingan sintaksisnya terbalik menjadi x < 10!
Hal ini dikarenakan dalam bahasa mesin, melompat lebih awal ketika kondisi tidak terpenuhi adalah tindakan yang lebih intuitif dan menghemat 1 instruksi dibandingkan dengan melakukan perbandingan sekali saat kondisi terpenuhi, lalu memeriksa kembali apakah kondisi tersebut tidak terpenuhi.
Optimasi semacam ini sangat klasik, sehingga tidak seperti contoh strconv.Atoi di atas, pola ini sering muncul bahkan pada kompilator dengan tingkat optimasi yang cukup rendah, sehingga sangat berguna untuk dipahami.
Dengan demikian, jika kita menerapkan poin-poin ini, kita dapat memperoleh kode sumber yang berbeda tetapi 100% identik pada tingkat assembly.
Contoh Kode Bayangan (Mirror Code)
Dengan menggunakan skrip di bawah ini, skrip bash dapat memverifikasi bahwa setelah membuat dua sumber yang 100% merupakan bayangan satu sama lain, assembly yang diperoleh akan benar-benar identik jika kita hanya melihat main setelah mengecualikan metadata yang berubah-ubah.
1#!/usr/bin/env bash
2
3# 1. Inisialisasi total file dan direktori sisa yang 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 bayangan (main_from_asm.go)
36# Sinkronisasi simetris sempurna pada struktur operator agar kompilator menggunakan templat 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 menjadikan 10 sebagai acuan dan mempertahankan struktur x < 10,
56 // kompilator akan mengadopsi 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 masing-masing dalam lingkungan jalur 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 fungsi assembly 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# Menghapus teks alamat virtual, offset, dan data byte mesin,
80# lalu memfilter hanya set instruksi murni (Opcode & Operands) yang akan dijalankan oleh CPU
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 diff struktur dua instruksi bahasa mesin
85echo "[6/6] Verifying assembly structural integrity via diff..."
86echo "------------------------------------------------------------"
87
88if diff orig_pure.asm asm_pure.asm > /dev/null; then
89 echo "===> [Berhasil] Logika bahasa mesin main.main dari kedua biner 100% identik! <==="
90 echo "Kami telah menyinkronkan panduan pipeline optimasi kompilator secara sempurna untuk mendapatkan assembly yang sama."
91else
92 echo "===> [Gagal] Ditemukan perbedaan dalam struktur instruksi assembly. <==="
93 diff -u orig_pure.asm asm_pure.asm
94fi
95echo "------------------------------------------------------------"
Jika Anda menjalankan sumber tersebut, Anda akan mendapatkan 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===> [Berhasil] Logika bahasa mesin main.main dari kedua biner 100% identik! <===
9Kami telah menyinkronkan panduan pipeline optimasi kompilator secara sempurna untuk mendapatkan assembly yang sama.
10------------------------------------------------------------
Kesimpulan
Bahasa pemrograman menyediakan banyak abstraksi, namun di balik abstraksi tersebut, kita dapat mengetahui bahwa terdapat optimasi yang sangat menarik dan agresif. Selain itu, dengan memanfaatkan hal ini, kita dapat membuat kode bayangan yang memiliki sumber berbeda namun memiliki assembly yang sama. Jika Anda tertarik pada tingkat rendah (low-level) dan menjumpai perangkat lunak eksklusif yang dibuat dengan Go, tampaknya bukan hal yang mustahil untuk memulihkan kode sumber dengan membongkar dan menganalisis assembly-nya secara langsung.
Materi Berikutnya
Pada sesi berikutnya, kita akan membahas pernyataan select-case yang memiliki keunikan tersendiri selain pernyataan If.