GoSuda

Pernyataan If dalam bahasa pemrograman Go

By Lee Yunjin
views ...

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.

struct8 byte8 byte
stringmem addressstring 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.