Halo Sobat, kali ini kita akan sedikit turun lebih dekat ke cara kerja komputer, yaitu mengenal bahasa Assembly.
Kalau selama ini kita mungkin lebih sering membuat program menggunakan PHP, C, Python, atau bahasa pemrograman tingkat tinggi lainnya, Assembly menawarkan pengalaman yang sedikit berbeda. Kita bisa melihat dengan lebih jelas bagaimana instruksi diberikan kepada prosesor melalui register, memori, dan instruksi yang sangat sederhana.
Assembly memang terlihat lebih rumit dibandingkan bahasa pemrograman modern. Namun, justru karena instruksinya sederhana, belajar Assembly bisa menjadi cara yang menarik untuk memahami apa yang sebenarnya terjadi di balik sebuah program.
Pada tutorial ini kita akan menggunakan:
Linux 64-bit
Prosesor x86-64
NASM sebagai assembler
GDB sebagai debugger
Contoh yang digunakan adalah Assembly dengan sintaks NASM. Jadi, jika Sobat menggunakan arsitektur atau sistem operasi berbeda, beberapa bagian kodenya bisa berbeda.
1. Apa Itu Assembly?
Assembly adalah bahasa pemrograman tingkat rendah yang instruksinya sangat dekat dengan instruksi yang dimengerti oleh prosesor.
Sederhananya, ketika kita menulis:
mov rax, 1
kita sedang memberikan instruksi kepada prosesor untuk memasukkan nilai 1 ke dalam register rax.
Berbeda dengan bahasa seperti PHP yang mempunyai banyak fungsi dan struktur tingkat tinggi, Assembly bekerja dengan instruksi-instruksi kecil seperti mov, add, sub, cmp, jmp, dan sebagainya.
Karena itu, Assembly sangat bergantung pada jenis prosesor yang digunakan. Assembly untuk x86-64 tidak sama dengan Assembly ARM, dan kode untuk Linux juga tidak selalu sama dengan kode untuk Windows.
2. Mengenal Register
Salah satu hal penting ketika mulai mengenal Assembly adalah register.
Register bisa dibayangkan sebagai tempat penyimpanan kecil yang berada sangat dekat dengan prosesor dan digunakan untuk menyimpan data yang sedang diproses.
Pada contoh x86-64, kita akan menemukan register seperti:
rax - sering digunakan untuk nilai hasil operasi atau menentukan nomor system call.
rdi - digunakan sebagai argumen pertama.
rsi - digunakan sebagai argumen kedua.
rdx - digunakan sebagai argumen ketiga.
Nama dan fungsi register sebenarnya cukup luas. Untuk tahap awal, kita tidak perlu menghafal semuanya. Kita bisa mengenalinya sambil melihat contoh program.
3. Membuat Program Hello World
Sekarang kita coba membuat program sederhana yang menampilkan tulisan Hello, World!.
Buat sebuah file, misalnya:
hello.asm
Kemudian isi dengan kode berikut:
section.data
pesan db 'Hello, World!', 10
panjang equ $ - pesan
section.text
global _start
_start:; Menampilkan teks ke layar
mov rax, 1
mov rdi, 1
mov rsi, pesan
mov rdx, panjang
syscall; Mengakhiri program
mov rax, 60
mov rdi, 0
syscall
Sobat mungkin langsung melihat bahwa kode tersebut cukup pendek, tetapi setiap baris mempunyai peran tertentu.
4. Mengenal section.data
Bagian berikut:
section.data
pesan db 'Hello, World!', 10
panjang equ $ - pesan
digunakan untuk menyimpan data yang sudah diketahui ketika program dibuat.
Pada contoh tersebut kita membuat sebuah data bernama pesan:
pesan db 'Hello, World!', 10
db merupakan singkatan dari define byte. Kita bisa menggunakannya untuk menyimpan data dalam bentuk byte.
Angka 10 di bagian akhir merupakan karakter newline atau pindah baris.
Kemudian:
panjang equ $ - pesan
digunakan untuk menghitung panjang data pesan secara otomatis. Tanda $ pada konteks ini menunjukkan posisi saat ini sehingga kita tidak perlu menghitung sendiri berapa banyak byte yang digunakan oleh teks tersebut.
5. Mengenal section.text
Berikutnya ada:
section.text
Bagian .text digunakan untuk menyimpan instruksi program yang nantinya dijalankan oleh prosesor.
Kemudian kita menentukan titik awal program:
global _start
dan:
_start:
Label _start menjadi titik awal eksekusi program pada contoh ini.
Di Linux x86-64, nomor 1 pada rax digunakan untuk write.
Kita kemudian mengisi beberapa register yang menjadi parameter:
rax = 1 → system call yang digunakan adalah write.
rdi = 1 → tujuan output adalah stdout.
rsi = pesan → alamat data yang akan ditulis.
rdx = panjang → jumlah byte yang akan ditulis.
Setelah semuanya siap, instruksi:
syscall
digunakan untuk meminta kernel Linux menjalankan system call tersebut.
Jadi, Assembly pada contoh ini tidak langsung berbicara kepada layar. Program meminta bantuan kernel Linux melalui system call write.
7. Mengakhiri Program
Setelah tulisan ditampilkan, program perlu dihentikan.
mov rax, 60
mov rdi, 0
syscall
Di Linux x86-64, nomor 60 adalah system call exit.
Sementara rdi = 0 berarti program keluar dengan status 0, yang biasanya digunakan untuk menunjukkan bahwa program selesai tanpa error.
8. Memasang NASM
Jika NASM belum tersedia di Linux, pada Ubuntu atau Debian kita bisa memasangnya dengan:
sudo apt update
sudo apt install nasm
Setelah itu periksa versinya:
nasm -v
Jika NASM sudah terpasang, kita bisa mulai merakit file Assembly tadi.
9. Merakit Program Assembly
Assembly perlu diubah terlebih dahulu menjadi object file menggunakan NASM.
nasm -f elf64 hello.asm -o hello.o
Parameter -f elf64 memberitahu NASM bahwa kita ingin membuat object file dengan format ELF 64-bit yang digunakan Linux x86-64.
Sekarang kita mempunyai:
hello.o
Object file tersebut kemudian perlu di-link menjadi executable.
ld hello.o -o hello
Setelah proses tersebut selesai, kita bisa menjalankan program:
./hello
Hasilnya:
Hello, World!
10. Mengapa Tidak Langsung Menjalankan File.asm?
File .asm masih berupa kode sumber yang berisi instruksi dalam bentuk teks yang bisa dibaca manusia.
Prosesnya secara sederhana seperti ini:
NASM bertugas menerjemahkan instruksi Assembly menjadi kode mesin di dalam object file. Setelah itu linker seperti ld menyusun object file tersebut menjadi executable yang dapat dijalankan Linux.
11. Mulai Mengenal Debugging
Nah, setelah program berhasil dibuat, bagian yang tidak kalah menarik adalah melihat apa yang sebenarnya terjadi ketika program berjalan.
Untuk itu kita bisa menggunakan GDB.
GDB memungkinkan kita menghentikan program pada titik tertentu, melihat isi register, melihat instruksi yang sedang dijalankan, dan menjalankan program sedikit demi sedikit.
Pada Assembly, kemampuan seperti ini cukup menarik karena kita bisa melihat langsung perubahan register ketika sebuah instruksi dijalankan.
12. Memasang GDB
Pada Ubuntu atau Debian:
sudo apt install gdb
Kemudian periksa:
gdb --version
13. Membuat Object File dengan Informasi Debug
Agar GDB bisa memberikan informasi yang lebih mudah kita ikuti, kita bisa menambahkan informasi debug ketika melakukan assembly.
nasm -f elf64 -g -F dwarf hello.asm -o hello.o
Kemudian lakukan linking:
ld hello.o -o hello
Dengan informasi debug tersebut, GDB dapat lebih mudah menghubungkan instruksi mesin dengan lokasi pada source Assembly.
14. Menjalankan Program dengan GDB
Kita bisa membuka program dengan:
gdb./hello
Setelah masuk ke GDB, kita bisa melihat kode Assembly dengan:
layout asm
Jika tampilan terminal Sobat tidak mendukung mode tersebut dengan nyaman, kita juga bisa menggunakan perintah biasa:
disassemble _start
GDB kemudian akan menampilkan instruksi yang terdapat pada fungsi atau label _start.
15. Membuat Breakpoint
Salah satu fitur utama debugger adalah breakpoint.
Kita bisa meminta GDB berhenti di _start:
break _start
Kemudian jalankan program:
run
Program akan berhenti ketika mencapai breakpoint tersebut.
Di sinilah kita bisa mulai melihat isi register sebelum instruksi dijalankan.
16. Melihat Isi Register
Untuk melihat register:
info registers
Atau jika hanya ingin melihat register tertentu:
info registers rax rdi rsi rdx
Pada awal program, nilainya mungkin belum seperti yang kita inginkan. Setelah instruksi mov dijalankan, kita bisa melihat nilainya berubah.
17. Menjalankan Instruksi Satu per Satu
Bagian ini yang menurut saya cukup menyenangkan ketika belajar Assembly.
GDB menyediakan perintah:
stepi
atau bentuk singkatnya:
si
Perintah tersebut menjalankan satu instruksi mesin kemudian berhenti lagi.
Misalnya kita mempunyai:
Kita bisa menjalankan si berulang kali sambil melihat register.
Misalnya setelah:
mov rax, 1
kita periksa:
info registers rax
dan akan terlihat bahwa rax sekarang berisi nilai 1.
Kemudian jalankan lagi:
si
Setelah mov rdi, 1, kita bisa melihat:
info registers rdi
Nilainya menjadi 1.
Dengan cara seperti ini, kita bisa mengikuti program sedikit demi sedikit tanpa harus menebak apa yang terjadi.
18. Melihat Isi Memori
Karena rsi menyimpan alamat data pesan, kita juga bisa melihat isi memori yang ditunjuk oleh register tersebut.
Contohnya:
x/s $rsi
Perintah x digunakan untuk memeriksa isi memori, sedangkan /s meminta GDB menampilkannya sebagai string.
Ini cukup membantu ketika kita sedang mencari tahu apakah alamat dan data yang digunakan program sudah benar.
19. Melihat Instruksi Mesin
Kita juga bisa meminta GDB menampilkan instruksi yang sedang digunakan:
disassemble _start
Atau melihat instruksi pada posisi program saat ini:
x/i $rip
rip adalah instruction pointer pada x86-64, yaitu register yang menunjukkan lokasi instruksi yang sedang atau akan dieksekusi.
20. Contoh Alur Debugging Sederhana
Kalau ingin mencoba dari awal, kira-kira alurnya seperti ini.
break _start
run
info registers
si
info registers rax
si
info registers rdi
si
info registers rsi
si
info registers rdx
si
Kita bisa mengulanginya sambil memperhatikan perubahan register.
Setelah selesai, keluar dari GDB dengan:
quit
21. Bagaimana Jika Program Error?
Di sinilah debugger menjadi sangat berguna.
Misalnya program tidak menampilkan teks, keluar terlalu cepat, atau bahkan mengalami segmentation fault. Daripada hanya melihat hasil akhirnya, kita bisa menghentikan program dan melihat keadaan internalnya.
Beberapa hal yang biasanya menarik untuk diperiksa:
Apakah rax berisi nomor system call yang benar?
Apakah rdi berisi argumen yang benar?
Apakah rsi menunjuk ke alamat data yang benar?
Apakah rdx berisi panjang data yang benar?
Apakah rip sedang menunjuk ke instruksi yang kita harapkan?
Dengan melihat nilai tersebut secara langsung, sumber masalah biasanya menjadi lebih mudah dicari.
22. Assembly Tidak Harus Langsung Dihafalkan
Saat pertama kali melihat Assembly, banyaknya register dan instruksi mungkin terlihat cukup membingungkan.
Menurut saya, tidak perlu terburu-buru menghafal semuanya.
Kita bisa mulai dari beberapa instruksi sederhana:
mov
add
sub
cmp
jmp
call
ret
push
pop
syscall
Kemudian sambil mencoba program kecil, kita akan mulai terbiasa melihat bagaimana instruksi tersebut bekerja.
Yang menarik dari Assembly justru kita bisa melihat hubungan antara instruksi, register, memori, dan prosesor secara lebih nyata.
23. Penutup
Jadi, Sobat, Assembly memang termasuk bahasa yang cukup dekat dengan perangkat keras. Karena itu pula, cara menulis programnya terasa berbeda dari bahasa pemrograman yang biasa kita gunakan.
Pada contoh sederhana tadi, kita sudah melihat beberapa konsep penting:
.data untuk menyimpan data.
.text untuk instruksi program.
Register seperti rax, rdi, rsi, dan rdx.
syscall sebagai pintu masuk program untuk meminta layanan kernel Linux.
NASM untuk merakit source Assembly menjadi object file.
ld untuk melakukan linking menjadi executable.
GDB untuk melihat dan men-debug program secara langsung.
si untuk menjalankan instruksi satu per satu.
info registers untuk melihat keadaan register.
Kalau sudah terbiasa dengan konsep dasar tersebut, langkah berikutnya bisa mulai mencoba operasi sederhana seperti penjumlahan, percabangan, perulangan, membaca input dari keyboard, hingga membuat program yang berinteraksi lebih banyak dengan kernel Linux.
Yang penting, nikmati prosesnya. Assembly memang membawa kita sedikit lebih dekat untuk melihat apa yang sebenarnya dilakukan komputer ketika sebuah program dijalankan.
Komentar 0