什麼是組合語言?為什麼要學它?
你每天用的手機 App、電腦軟體,大多是用 Python、Java、C++ 這些「高階語言」寫的。但電腦的 CPU 其實看不懂這些語言——它只認得 1 和 0(也就是二進位)。
那高階語言怎麼跑起來的?答案是:經過層層翻譯,最終變成 CPU 能理解的「機器碼」。
組合語言(Assembly Language) 就是最接近機器碼、但人類還勉強看得懂的語言。它長這樣:
mov rax, 1 ; 把數字 1 放進 rax 暫存器
同一條指令,如果用機器碼的十六進位表示就是 48 c7 c0 01,二進位則是 01001000 11000111 11000000 00000001。相比之下,Assembly 明顯更容易理解。
翻譯的層級
想像一下「Hello World!」從高階到低階的旅程:
Python: print("Hello World!")
↓ 直譯器呼叫 C 函式庫
C: write(1, "Hello World!", 12);
↓ 編譯器翻譯
Assembly: mov rax, 1 / mov rdi, 1 / ... / syscall
↓ 組譯器翻譯
Hex: 48 c7 c0 01 ...
↓
Binary: 01001000 11000111 ... ← CPU 實際執行的東西
學組合語言有什麼用?
如果你對資安、逆向工程、漏洞利用(Buffer Overflow、ROP Chain 等)有興趣,組合語言是必修課。因為:
- 分析惡意程式需要讀懂反組譯後的 Assembly
- 撰寫漏洞利用程式(exploit)需要用 Assembly 來操作記憶體
- 了解程式在 CPU 中的實際運作方式
環境準備
本教學使用 Linux(Ubuntu / Debian) 搭配 NASM 組譯器 和 x86-64 架構。
打開終端機,安裝必要工具:
# 更新套件列表
sudo apt update
# 安裝 NASM(組譯器)和 ld(連結器,通常已內建)
sudo apt install -y nasm build-essential
# 確認安裝成功
nasm --version
ld --version
你也可以選擇安裝 gdb(除錯器),後面會用到:
sudo apt install -y gdb
基礎概念速覽
在寫程式之前,先了解幾個關鍵概念。
暫存器(Registers)
暫存器是 CPU 內部的超高速儲存空間。x86-64 常用的有:
| 暫存器 | 用途說明 |
|---|---|
rax |
通用暫存器,常用來放回傳值和系統呼叫編號 |
rbx |
通用暫存器,常作為基底位址 |
rcx |
計數器,迴圈中常用 |
rdx |
資料暫存器,也用來傳遞系統呼叫的第三個參數 |
rsi |
來源索引,系統呼叫的第二個參數 |
rdi |
目的索引,系統呼叫的第一個參數 |
rsp |
堆疊指標(Stack Pointer) |
rbp |
基底指標(Base Pointer) |
系統呼叫(Syscall)
在 Linux 中,程式透過 syscall 指令向作業系統請求服務(寫入螢幕、讀取檔案、結束程式等)。
系統呼叫的規則:
– rax = 系統呼叫編號(例如 1 是 write,60 是 exit)
– rdi = 第一個參數
– rsi = 第二個參數
– rdx = 第三個參數
– 然後執行 syscall
資料區段
Assembly 程式通常分成幾個區段:
– .text:放程式碼(指令)
– .data:放已初始化的資料(字串、常數)
– .bss:放未初始化的資料(預留空間)
實戰一:Hello World!
來寫你的第一支 Assembly 程式!
第一步:撰寫程式碼
用你喜歡的編輯器建立檔案 hello.asm:
vim hello.asm
輸入以下內容:
; hello.asm - 你的第一支 Assembly 程式
; 功能:在螢幕上印出 "Hello World!"
section .data
msg db "Hello World!", 0x0a ; 定義字串,0x0a 是換行符號
msg_len equ $ - msg ; 自動計算字串長度
section .text
global _start ; 告訴連結器程式的進入點
_start:
; --- 呼叫 write(1, msg, msg_len) ---
mov rax, 1 ; syscall 編號 1 = write(寫入)
mov rdi, 1 ; 檔案描述子 1 = stdout(標準輸出,也就是螢幕)
mov rsi, msg ; 要寫入的字串的位址
mov rdx, msg_len ; 要寫入的字元數
syscall ; 執行系統呼叫!
; --- 呼叫 exit(0) ---
mov rax, 60 ; syscall 編號 60 = exit(結束程式)
mov rdi, 0 ; 回傳碼 0 = 正常結束
syscall ; 執行系統呼叫!
第二步:組譯與連結
# 組譯:把 .asm 轉成目的檔 .o
nasm -f elf64 hello.asm -o hello.o
# 連結:把 .o 轉成可執行檔
ld hello.o -o hello
參數說明:
– -f elf64:輸出 64 位元 ELF 格式(Linux 的執行檔格式)
– -o:指定輸出檔名
第三步:執行
./hello
你應該會在螢幕上看到:
Hello World!
恭喜!你已經成功用 Assembly 寫出了第一支程式!
逐行解說
讓我們回頭看看每一行到底做了什麼:
section .data
宣告資料區段,用來存放程式需要用到的資料。
msg db "Hello World!", 0x0a
db 是 “define byte”,定義了一串位元組。0x0a 是換行字元(\n)的十六進位值。
msg_len equ $ - msg
equ 定義一個常數。$ 代表「目前位置」,減去 msg 的位置,就自動算出字串長度。
mov rax, 1
mov 指令把右邊的值搬到左邊。這裡把數字 1 放入 rax,告訴系統我們要執行 write 系統呼叫。
syscall
觸發系統呼叫,CPU 會根據 rax 的值去執行對應的功能。
實戰二:讀取使用者輸入
接下來,我們寫一個會先問你名字、再跟你打招呼的程式。
建立 greet.asm:
; greet.asm - 讀取使用者輸入並回應
section .data
prompt db "What is your name? ", 0
prompt_len equ - prompt
hello db "Hello, ", 0
hello_len equ - hello
newline db 0x0a
newline_len equ 1
section .bss
name resb 64 ; 預留 64 bytes 給使用者輸入
section .text
global _start
_start:
; --- 印出提示文字 ---
mov rax, 1
mov rdi, 1
mov rsi, prompt
mov rdx, prompt_len
syscall
; --- 讀取使用者輸入 ---
mov rax, 0 ; syscall 編號 0 = read(讀取)
mov rdi, 0 ; 檔案描述子 0 = stdin(標準輸入,也就是鍵盤)
mov rsi, name ; 把讀到的內容存到 name
mov rdx, 64 ; 最多讀 64 bytes
syscall
mov r8, rax ; 把實際讀到的字元數存到 r8(read 的回傳值在 rax)
; --- 印出 "Hello, " ---
mov rax, 1
mov rdi, 1
mov rsi, hello
mov rdx, hello_len
syscall
; --- 印出使用者的名字 ---
mov rax, 1
mov rdi, 1
mov rsi, name
mov rdx, r8 ; 用之前存的字元數
syscall
; --- 結束程式 ---
mov rax, 60
mov rdi, 0
syscall
組譯並執行:
nasm -f elf64 greet.asm -o greet.o
ld greet.o -o greet
./greet
執行結果會像這樣:
What is your name? fei
Hello, fei
新學到的東西
section .bss和resb 64:在.bss區段預留 64 bytes 的未初始化空間,用來存放使用者輸入。- read 系統呼叫(編號 0):從鍵盤讀取輸入。它的回傳值(放在
rax)告訴我們實際讀了多少字元。 r8:另一個通用暫存器,我們用它暫存讀取的長度,因為下一個 syscall 會覆蓋rax。
實戰三:簡單的加法計算
我們來做一個把兩個數字相加、印出結果的程式。為了簡化,這裡只處理個位數。
建立 add.asm:
; add.asm - 計算 3 + 5 並印出結果
section .data
msg db "3 + 5 = ", 0
msg_len equ $ - msg
newline db 0x0a
section .bss
result resb 2 ; 預留空間放結果字元
section .text
global _start
_start:
; --- 印出 "3 + 5 = " ---
mov rax, 1
mov rdi, 1
mov rsi, msg
mov rdx, msg_len
syscall
; --- 做加法 ---
mov rax, 3 ; rax = 3
add rax, 5 ; rax = rax + 5 = 8
; --- 把數字轉成 ASCII 字元 ---
; ASCII 中 '0' 的值是 48,所以數字 + 48 就變成對應的字元
add rax, 48 ; 8 + 48 = 56 = ASCII '8'
mov [result], al ; 把結果的最低位元組存入 result
mov byte [result+1], 0x0a ; 加上換行
; --- 印出結果 ---
mov rax, 1
mov rdi, 1
mov rsi, result
mov rdx, 2
syscall
; --- 結束 ---
mov rax, 60
mov rdi, 0
syscall
nasm -f elf64 add.asm -o add.o
ld add.o -o add
./add
輸出:
3 + 5 = 8
新學到的東西
add rax, 5:加法指令,把 5 加到rax的值上。- ASCII 轉換:電腦用 ASCII 碼來表示字元。數字 0-9 對應的 ASCII 值是 48-57,所以
數字 + 48= 對應的字元。 [result]:方括號代表「記憶體位址的內容」。mov [result], al意思是「把al(rax 的最低 8 位元)的值,寫入 result 這個位址」。al:rax的最低 8 位元部分。64 位元暫存器可以用不同名稱存取不同大小:rax(64 位元)→eax(32 位元)→ax(16 位元)→al(低 8 位元)。
用 GDB 除錯你的程式
寫 Assembly 常常會出錯。GDB(GNU Debugger)是你的好朋友,可以讓你一步一步執行程式、觀察暫存器的變化。
先用 -g 參數重新組譯(加入除錯資訊):
nasm -f elf64 -g hello.asm -o hello.o
ld hello.o -o hello
啟動 GDB:
gdb ./hello
常用指令:
(gdb) break _start # 在 _start 設定中斷點
(gdb) run # 開始執行
(gdb) stepi # 執行一條指令(step instruction)
(gdb) info registers # 查看所有暫存器的值
(gdb) print/x $rax # 以十六進位印出 rax 的值
(gdb) x/s &msg # 查看 msg 位址的字串內容
(gdb) quit # 離開 GDB
試試看在每一步 stepi 後用 info registers 觀察暫存器的變化,你會對程式的運作有更直覺的理解。
常用指令速查表
| 指令 | 說明 | 範例 |
|---|---|---|
mov a, b |
把 b 的值複製到 a | mov rax, 1 |
add a, b |
a = a + b | add rax, 5 |
sub a, b |
a = a – b | sub rax, 3 |
inc a |
a = a + 1 | inc rcx |
dec a |
a = a – 1 | dec rcx |
mul b |
rax = rax × b(無號數) | mul rbx |
cmp a, b |
比較 a 和 b(設定旗標) | cmp rax, 10 |
jmp label |
無條件跳轉 | jmp loop_start |
je label |
相等時跳轉 | je done |
jne label |
不相等時跳轉 | jne loop_start |
jl label |
小於時跳轉 | jl negative |
jg label |
大於時跳轉 | jg positive |
syscall |
執行系統呼叫 | syscall |
常用 Linux 系統呼叫(x86-64)
| rax 編號 | 名稱 | rdi | rsi | rdx |
|---|---|---|---|---|
| 0 | read | 檔案描述子 | 緩衝區位址 | 讀取長度 |
| 1 | write | 檔案描述子 | 字串位址 | 寫入長度 |
| 60 | exit | 結束碼 | — | — |
檔案描述子:0 = stdin(鍵盤),1 = stdout(螢幕),2 = stderr(錯誤輸出)
接下來可以學什麼?
掌握了基礎之後,建議的學習路徑:
- 條件判斷與迴圈:用
cmp+jmp系列指令實作 if/else 和 for 迴圈 - 函式與堆疊:學習
push、pop、call、ret,理解函式呼叫的機制 - 記憶體操作:深入了解定址模式(addressing modes)
- Shellcoding:學會撰寫可注入的機器碼,這是漏洞利用的核心技能
- 逆向工程:用
objdump、gdb、Ghidra 等工具分析已編譯的程式
總結
組合語言不像 Python 那樣友善,但它讓你真正理解電腦底層的運作方式。每一條指令都直接對應 CPU 的動作,沒有任何魔法。
記住核心流程:寫 .asm → nasm 組譯 → ld 連結 → 執行。
多寫、多用 GDB 觀察、多查系統呼叫表,你會越來越熟悉。












