x86 與 x86-64 的 NASM

最後更新: 2025-01-13 21:14:39 +0800 +0800
tags: Nasm | Assembly | X86
原文以英文撰寫。您正在閱讀AI翻譯版本。

NASM 將 Intel 語法的 x86 組合語言轉換為平面二進位檔或物件檔。平面二進位檔包含輸出的位元組,但沒有標準重新定位或符號中繼資料。ELF、Mach-O 或 COFF 等物件檔會保留區段、符號與重新定位資訊,使連結器能將其與其他物件及函式庫組合。

區段、標籤與符號 #

物件檔來源通常會分開可執行程式碼、已初始化資料與零初始化儲存空間:

nasm
global add_one
extern external_value

section .text
add_one:
    mov eax, edi
    add eax, 1
    ret

section .rodata
message: db "hello", 10
message_size equ $ - message

section .bss
scratch: resq 8

標籤定義目前的組譯位置。GLOBAL 將符號匯出給連結器,而 EXTERN 宣告另一個物件必須定義的符號。以 . 開頭的區域標籤會限制在前一個非區域標籤的範圍內。區段名稱與屬性取決於輸出格式;.text、.rodata、.data 與 .bss 是 ELF 慣例,不是 NASM 的通用關鍵字。

使用 -f bin 選取的平面二進位檔沒有連結器可指派虛擬位址。ORG 會變更位址計算使用的起始位置,但不會插入填充或將位元組載入該位址。開機磁區、韌體與嵌入式映像檔必須建立自己的配置與填充需求。

有效位址與位置獨立性 #

NASM 將目的運算元放在前面。方括號表示記憶體有效位址:

nasm
mov rax, value          ; address/immediate determined by relocation rules
mov rbx, [value]        ; load from memory
mov ecx, [base + index*4 + 8]
lea rdx, [base + index*4 + 8]  ; calculate address; do not load

x86 位址允許基底、以 1、2、4 或 8 縮放的索引,以及位移,但受模式特定編碼規則約束。當沒有暫存器決定大小時,記憶體運算元可能需要 byte、dword 或 qword 等明確限定詞。

BITS 16、BITS 32 與 BITS 64 選取目標指令編碼模式,不會在執行時切換處理器。物件格式會設定適當預設值;-f bin 預設為 16 位元模式,因此平面 32 或 64 位元程式碼應明確指定模式。

在 64 位元模式中,DEFAULT REL 會讓無暫存器記憶體參照使用 RIP 相對位址,除非由 ABS 覆寫;FS 與 GS 覆寫具有 NASM 文件記載的特殊預設值。RIP 相對參照可以到達 ±2 GiB 內的程式碼或靜態資料,適合位置獨立程式碼。位置獨立性也取決於物件格式、重新定位類型、程式碼模型、符號可見性與連結器。單靠 DEFAULT REL 不能讓任意外部符號或絕對位址使用變成位置獨立。

定義、保留與對齊資料 #

DB、DW、DD、DQ、DT、DO、DY 與 DZ 會輸出大小遞增的已初始化值。常見定義包括:

nasm
bytes:  db 0x7f, "NASM", 0
words:  dw 1, 2, 3
value:  dq 42
table:  times 16 dd 0

RESB、RESW、RESD 與 RESQ 會在支援的區段中保留未初始化儲存空間。EQU 定義組譯時常數,$ 表示目前位置。保留空間不一定會為物件檔增加位元組,因為類似 BSS 的區段記錄的是大小而不是已初始化內容。

ALIGN n 會前進至要求的邊界,並根據其引數與作用中區段填補間隙。物件中的對齊也取決於區段對齊與連結器配置,因此對齊不足區段內的對齊偏移並不充分。整個區段需要更強保證時,請使用 SECTION ... align=n 或格式特定機制。

前處理器巨集與 Include #

NASM 在組譯前執行以權杖為基礎的前處理器:

  • %define 與 %xdefine 定義單行巨集,但展開時機不同。
  • %macro 與 %endmacro 定義多行巨集;%1、%2 等名稱代表位置引數。
  • %if、%ifdef、%elif 與 %endif 有條件地選取來源。
  • %include 讀取另一個來源檔,而 -I 加入 include 搜尋路徑。
  • %assign 維護數值前處理器變數。
nasm
%ifndef WORD_SIZE
    %define WORD_SIZE 8
%endif

%macro save_pair 2
    push %1
    push %2
%endmacro

巨集產生的是來源,而不是可呼叫例程。它們可能重複程式碼、捕捉名稱或產生令人意外的診斷,因此檢查展開時,清單(-l file.lst)與反組譯很有用。當共用巨集檔案被多次 include 時,include guard 可防止多重定義。

針對 ABI 組譯與連結 #

輸出格式與 ABI 是分開的選擇。在 64 位元 Linux System V 目標上:

sh
nasm -f elf64 -g -F dwarf -o add_one.o add_one.asm
cc -o demo main.c add_one.o

在 32 位元 Linux 目標上,使用 -f elf32 與設定好必要 32 位元執行階段的連結器/編譯器,例如 cc -m32。Windows x64 使用 win64 與 Microsoft x64 呼叫慣例;macOS 使用 macho64 與 Mach-O 符號規則。物件檔無法跨這些格式攜帶。

手寫函式必須實作目標 ABI 的引數暫存器、堆疊對齊、回傳值、保留暫存器、展開中繼資料與符號命名。Linux syscall 序列遵循核心系統呼叫 ABI,而不是使用者空間函式 ABI。當需要 C 或 C++ 執行階段啟動、預設函式庫、位置獨立可執行檔政策或平台連結器選項時,請使用編譯器驅動程式連結。

NASM 的目前手冊定義指示詞與格式特定行為。作業系統 ABI 與處理器手冊定義組譯器之外的契約。

評論

在 GitHub 上閱讀或評論(發表評論需要登入)