[逆向工程] 003 Assembly 組合語言新手教學:從 Hello World 到讀取輸入與加法運算

什麼是組合語言?為什麼要學它?

你每天用的手機 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 .bssresb 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 這個位址」。
  • alrax 的最低 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(錯誤輸出)


接下來可以學什麼?

掌握了基礎之後,建議的學習路徑:

  1. 條件判斷與迴圈:用 cmp + jmp 系列指令實作 if/else 和 for 迴圈
  2. 函式與堆疊:學習 pushpopcallret,理解函式呼叫的機制
  3. 記憶體操作:深入了解定址模式(addressing modes)
  4. Shellcoding:學會撰寫可注入的機器碼,這是漏洞利用的核心技能
  5. 逆向工程:用 objdumpgdb、Ghidra 等工具分析已編譯的程式

總結

組合語言不像 Python 那樣友善,但它讓你真正理解電腦底層的運作方式。每一條指令都直接對應 CPU 的動作,沒有任何魔法。

記住核心流程:.asmnasm 組譯 → ld 連結 → 執行

多寫、多用 GDB 觀察、多查系統呼叫表,你會越來越熟悉。

飛飛
飛飛

講師學歷:臺科資工所、逢甲資工系畢業。
技術專長:OSINT、滲透測試、網站開發、專業易懂教育訓練。
證照書籍:OSCP、OSCE³、著《資安這條路:領航新手的 Web Security 指南》。
教學經驗:60+ 企業教學經驗、指導過上百位學員。
教學特色:新手友善、耐心指導、擅長圖解(流程圖、心智圖)引導學習。
社群經驗:目前經營全臺資安社群 CURA,曾任臺科資安社社長、逢甲黑客社社長。
社群交流:LINE 社群《飛飛的資安大圈圈》,即時分享經驗、鼓勵交流。
社群分享:FB 粉專《資安這條路,飛飛來領路》,分享文章與圖卡整理。
個人網站:feifei.tw 分享資安技術文章;pbtw.tw 分享 AI 相關應用;ssdlc.feifei.tw 分享軟體安全開發流程文章。