objdump dan Compiler Explorer

Membaca keluaran assembler dan compiler nyata melalui objdump lokal dan Compiler Explorer daring.

Daftar isi

Menulis assembly sendiri melatih penguasaan instruksi, tetapi pekerjaan analisis pada umumnya dimulai dari assembly yang dihasilkan compiler atau dari executable yang sudah jadi. Topik ini membahas dua perkakas untuk keperluan tersebut, yaitu objdump yang membongkar kode mesin menjadi teks assembly pada komputer lokal dan Compiler Explorer yang melakukan hal yang sama secara daring. Pembahasan mencakup cara membaca keluaran objdump pada executable maupun berkas objek, opsi objdump yang paling sering digunakan, alur kerja Compiler Explorer, serta perbandingan keluaran compiler untuk satu fungsi kecil pada dua level optimasi.

Membaca executable dengan objdump#

objdump adalah disassembler dari paket binutils, yaitu perkakas yang membaca kode mesin lalu menampilkannya kembali sebagai teks assembly. Opsi -d membongkar seluruh section yang berisi kode, sedangkan opsi -M intel memilih notasi Intel sehingga keluarannya sejalan dengan notasi yang digunakan sepanjang jalur ini. Program kecil berikut menjadi bahan latihan untuk bagian ini.

nasm jumlah.asm
; jumlah.asm
; Menjumlahkan dua konstanta lalu mengakhiri proses dengan hasilnya.
default rel
section .text
global _start
_start:
    mov eax, 20
    add eax, 22
    mov edi, eax
    mov eax, 60
    syscall

Program di atas menjumlahkan dua konstanta, yaitu 20 dan 22, lalu mengakhiri proses dengan hasil penjumlahannya sebagai kode keluar. Setelah dirakit, ditautkan, dan dijalankan, kode keluar 42 pada keluaran berikut mengonfirmasi bahwa penjumlahan terjadi sebagaimana dimaksud.

bash
nasm -f elf64 jumlah.asm -o jumlah.o
ld jumlah.o -o jumlah
./jumlah; echo "exit=$?"
text Keluaran
exit=42

Perintah berikut membongkar executable yang baru dibangun dengan notasi Intel.

bash
objdump -d -M intel jumlah
text Keluaran

jumlah:     file format elf64-x86-64


Disassembly of section .text:

0000000000401000 <_start>:
  401000:	b8 14 00 00 00       	mov    eax,0x14
  401005:	83 c0 16             	add    eax,0x16
  401008:	89 c7                	mov    edi,eax
  40100a:	b8 3c 00 00 00       	mov    eax,0x3c
  40100f:	0f 05                	syscall

Setiap baris keluaran tersusun atas tiga bagian, yaitu alamat memori instruksi, byte kode mesin yang benar-benar tersimpan, dan mnemonic beserta operandnya. Pada baris pertama, byte b8 14 00 00 00 adalah instruksi mov eax,0x14, dan 0x14 merupakan nilai heksadesimal dari 20. Baris berikutnya menambahkan 0x16, yaitu 22, sehingga eax bernilai 42 ketika instruksi syscall dijalankan. Apabila kolom alamat, byte, dan mnemonic dibaca secara berdampingan, hubungan antara teks assembly dan representasi mesinnya terlihat tanpa alat tambahan.

Berkas objek dan opsi penyesuaian#

objdump juga dapat membongkar berkas objek, yaitu hasil perakitan yang alamatnya belum diselesaikan oleh linker. Perintah berikut membongkar jumlah.o dalam notasi AT&T bawaan, kemudian menampilkan isi section .text dengan opsi -s.

bash
objdump -d jumlah.o
objdump -s -j .text jumlah
text Keluaran

jumlah.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <_start>:
   0:	b8 14 00 00 00       	mov    $0x14,%eax
   5:	83 c0 16             	add    $0x16,%eax
   8:	89 c7                	mov    %eax,%edi
   a:	b8 3c 00 00 00       	mov    $0x3c,%eax
   f:	0f 05                	syscall

jumlah:     file format elf64-x86-64

Contents of section .text:
 401000 b8140000 0083c016 89c7b83c 0000000f  ...........<....
 401010 05                                   .               

Alamat pada keluaran pertama dimulai dari nol karena berkas objek belum memuat alamat final, dan pemetaan alamat baru ditetapkan saat penautan. Notasi AT&T pada keluaran itu menempatkan operan sumber sebelum operan tujuannya, sehingga mov $0x14,%eax adalah instruksi yang sama dengan mov eax,0x14 pada notasi Intel. Perbandingan dua keluaran ini memperlihatkan bahwa objdump hanya mengubah cara menampilkan byte yang sama, bukan isi programnya.

Opsi lain yang sering digunakan adalah --disassemble=nama, yang membatasi bongkaran hanya pada simbol yang disebut. Opsi ini tersedia sejak binutils 2.32 dan berguna pada executable besar yang berisi ratusan fungsi, sebab keluarannya hanya memuat fungsi yang diminta. Opsi -s bekerja secara berbeda karena tidak membongkar instruksi, melainkan menampilkan isi section dalam heksadesimal beserta bentuk teksnya pada kolom kanan, sebagaimana terlihat pada byte b8140000 di keluaran di atas.

Compiler Explorer#

Compiler Explorer adalah situs daring di godbolt.org yang mengompilasi kode dan menampilkan assembly-nya berdampingan dengan kode sumbernya. Alur penggunaannya berurutan, yaitu memilih bahasa dan compiler pada menu bagian atas, misalnya x86-64 gcc, kemudian menulis kode pada panel kiri sehingga panel kanan langsung menampilkan assembly yang dihasilkan compiler pilihan itu. Saat kursor diarahkan ke satu baris sumber, baris assembly yang sesuai ditandai dengan warna yang sama, sehingga hubungan keduanya dapat dilacak. Flag compiler dapat diatur, beberapa compiler dapat dijalankan berdampingan untuk membandingkan keluarannya, dan hasil percobaan dapat dibagikan melalui tautan permanen. Karena situs ini merupakan layanan daring, contoh Compiler Explorer tidak dieksekusi di sandbox dan digantikan dengan objdump lokal pada program setara.

Berkas berikut adalah contoh fungsi kecil yang dapat ditempelkan ke Compiler Explorer untuk mengamati assembly hasil gcc.

c kuadrat.c
#include <stdio.h>

int kuadrat(int x) {
    return x * x;
}

int main(void) {
    printf("%d\n", kuadrat(7));
    return 0;
}
text Keluaran
49

Perintah berikut melakukan hal yang sama secara lokal, yaitu mengompilasi berkas itu pada level -O0 dan -O2, menjalankan keduanya, lalu membongkar fungsi kuadrat dari masing-masing executable. Penyaring sed hanya membuang judul section dan baris kosong agar keluarannya pendek.

bash
gcc -O0 kuadrat.c -o kuadrat-o0
gcc -O2 kuadrat.c -o kuadrat-o2
./kuadrat-o0
./kuadrat-o2
# buang judul section dan baris kosong agar tersisa fungsi kuadrat
objdump -d -M intel --disassemble=kuadrat kuadrat-o0 | sed '/Disassembly of section/d;/^$/d'
objdump -d -M intel --disassemble=kuadrat kuadrat-o2 | sed '/Disassembly of section/d;/^$/d'
text Keluaran
49
49
kuadrat-o0:     file format elf64-x86-64
0000000000001139 <kuadrat>:
    1139:	55                   	push   rbp
    113a:	48 89 e5             	mov    rbp,rsp
    113d:	89 7d fc             	mov    DWORD PTR [rbp-0x4],edi
    1140:	8b 45 fc             	mov    eax,DWORD PTR [rbp-0x4]
    1143:	0f af c0             	imul   eax,eax
    1146:	5d                   	pop    rbp
    1147:	c3                   	ret
kuadrat-o2:     file format elf64-x86-64
0000000000001160 <kuadrat>:
    1160:	0f af ff             	imul   edi,edi
    1163:	89 f8                	mov    eax,edi
    1165:	c3                   	ret

Kedua program mencetak 49, tetapi assembly-nya berbeda jauh. Pada -O0, gcc menyusun kerangka stack lengkap dengan push rbp dan mov rbp,rsp, menyalin argumen ke stack, mengambilnya kembali ke eax, lalu mengalikannya melalui instruksi imul. Pada -O2, seluruh kerangka itu hilang dan tersisa tiga instruksi, yaitu imul edi,edi yang mengalikan argumen dengan dirinya sendiri, mov eax,edi yang menaruh hasil pada register balikan, dan ret. Pada fungsi main, gcc bahkan melipat perhitungan kuadrat saat kompilasi sehingga printf dipanggil dengan konstanta 49 yang sudah jadi. Perbedaan inilah yang membuat assembly hasil -O2 menuntut kebiasaan baca yang berbeda daripada assembly hasil -O0.

Kode sumber.c / .asmExecutableDisassemblyCompiler ExplorerdaringAssemblygcc / nasmobjdump -dpilih compiler, tulis kode
Dua jalur membaca assembly, yaitu objdump lokal dan Compiler Explorer daring

Kesalahan umum#

  • Keluaran AT&T dibaca dengan kebiasaan Intel. Penyebabnya adalah opsi -M intel yang ditiadakan, dan akibatnya operand terbaca terbalik, misalnya mov $0x14,%eax dianggap memindahkan eax ke konstanta. Perbaikannya adalah menambahkan -M intel pada setiap perintah objdump yang dijalankan.
  • Alamat pada berkas objek dianggap alamat final. Nilai 0 dan urutan alamat pada bongkaran jumlah.o bergeser setelah penautan, sehingga alamat tersebut tidak boleh digunakan untuk breakpoint atau perhitungan manual. Perbaikannya adalah membongkar executable yang sudah ditautkan setiap kali alamat nyata dibutuhkan.
  • --disassemble=nama tidak menghasilkan instruksi apa pun. Penyebabnya adalah nama simbol yang salah tulis atau executable yang telah di-strip sehingga tabel simbolnya hilang. Nama yang benar diperiksa lebih dahulu melalui objdump -t atau nm.

Ringkasan#

  • objdump dengan opsi -d membongkar kode mesin executable maupun berkas objek menjadi teks assembly, dan -M intel menampilkan notasi yang sejalan dengan jalur ini.
  • Alamat pada berkas objek belum final, sedangkan opsi --disassemble=nama membatasi bongkaran pada satu simbol dan -s menampilkan isi section dalam heksadesimal.
  • Compiler Explorer menyediakan jalur daring yang sama, yaitu kode sumber ditulis langsung dan assembly hasil compiler nyata dibaca berdampingan dengan sumbernya.
  • Level optimasi sangat memengaruhi bentuk assembly, karena -O0 mempertahankan kerangka stack sedangkan -O2 menghilangkannya dan melipat perhitungan konstanta.

Referensi#