x86 彙編/與 Linux 互動
系統呼叫是使用者程式和 Linux 核心之間的介面。它們用於讓核心執行各種系統任務,例如檔案訪問、程序管理和網路。在 C 程式語言中,您通常會呼叫一個包裝函式來執行所有必需的步驟,甚至使用高階功能,例如標準 IO 庫。
在 Linux 上,有幾種方法可以發出系統呼叫。此頁面將重點介紹透過使用 int $0x80 或 syscall 呼叫軟體中斷來發出系統呼叫。這是一種在純彙編程式中發出系統呼叫的簡單直觀的方法。
為了透過中斷髮出系統呼叫,您必須透過將所有必需的資訊複製到 GPRs 中來傳遞它們給核心。
每個系統呼叫都有一個固定的編號。Linux 永久保證向後相容性,因此一旦一個編號被分配給一個系統呼叫,它就不會再改變。永遠。
int $0x80 和 syscall 的編號不同! |
您可以透過將編號寫入 eax/rax 暫存器來指定系統呼叫。
大多數系統呼叫都採用引數來執行其任務。這些引數透過在發出實際呼叫之前將它們寫入相應的暫存器來傳遞。每個引數索引都有一個特定的暫存器。請檢視小節中的表格,因為 int $0x80 和 syscall 之間的對映不同。引數按它們在相應 C 包裝函式的函式簽名中出現的順序傳遞。您可以在每個 Linux ABI 文件中找到系統呼叫函式及其簽名,例如參考手冊(鍵入 man 2 open 檢視 open 系統呼叫的簽名)。
在一切設定正確後,您可以使用 int $0x80 或 syscall 呼叫中斷,核心將執行任務。
系統呼叫的返回值/錯誤值將寫入 eax/rax。
核心使用自己的堆疊來執行操作。使用者堆疊不會以任何方式被觸碰。
在 Linux x86 和 Linux x86_64 系統上,您可以透過使用 int 指令呼叫中斷 $0x80 來發出系統呼叫。引數透過設定如下通用暫存器來傳遞
| 系統呼叫編號 | 第一個引數 | 第二個引數 | 第三個引數 | 第四個引數 | 第五個引數 | 第六個引數 | 結果 |
|---|---|---|---|---|---|---|---|
eax
|
ebx
|
ecx
|
edx
|
esi
|
edi
|
ebp
|
eax
|
系統呼叫編號在 Linux 生成的檔案 $build/arch/x86/include/generated/uapi/asm/unistd_32.h 或 $build/usr/include/asm/unistd_32.h 中描述。後者也可能存在於您的 Linux 系統中,只需省略 $build。
在使用 int $0x80 發出系統呼叫期間,所有暫存器都會被保留,除了 eax,返回值將儲存在那裡。
x86_64 架構引入了一條專用指令來發出系統呼叫。它不訪問中斷描述符表,並且速度更快。引數透過設定如下 GPRs 來傳遞
| 系統呼叫編號 | 第一個引數 | 第二個引數 | 第三個引數 | 第四個引數 | 第五個引數 | 第六個引數 | 結果 |
|---|---|---|---|---|---|---|---|
rax
|
rdi
|
rsi
|
rdx
|
r10
|
r8
|
r9
|
rax
|
系統呼叫編號在 Linux 生成的檔案 $build/usr/include/asm/unistd_64.h 中描述。此檔案也可能存在於您的 Linux 系統中,只需省略 $build。
在使用 syscall 發出系統呼叫期間,所有暫存器都會被保留,除了 rcx 和 r11(以及返回值 rax)。
為了實現最大相容性,在 64 位平臺上,Linux 會剪裁使用中斷方法的系統呼叫的輸入和輸出。這意味著,例如,您不能在使用 int $0x80 方法的 x86-64 平臺上傳遞或接收(完整的)64 位地址指標,因為所有引數和結果的高 32 位將被清零。這通常與 syscall 的一般偏好一致,因為它比中斷更快。
在 x86-64 Linux 的 C 庫函式呼叫中,引數 6 在 r9 上傳遞,後續引數在堆疊上傳遞(以相反順序)。
| 第一個引數 | 第二個引數 | 第三個引數 | 第四個引數 | 第五個引數 | 第六個引數 |
|---|---|---|---|---|---|
rdi
|
rsi
|
rdx
|
rcx
|
r8
|
r9
|
呼叫者可以預期在 rax 暫存器中找到子例程的返回值。
為了總結和澄清資訊,讓我們來看一個非常簡單的例子:hello world 程式。它將使用 write 系統呼叫將文字 "Hello World" 寫入標準輸出,並使用 _exit 系統呼叫退出程式。
系統呼叫簽名
ssize_t write(int fd, const void *buf, size_t count);
void _exit(int status);
這是在下面彙編中實現的 C 程式
#include <unistd.h>
int main(int argc, char *argv[])
{
write(1, "Hello World\n", 12); /* write "Hello World" to stdout */
_exit(0); /* exit with error code 0 (no error) */
}
兩個示例的開頭都一樣:一個儲存在資料段中的字串和作為全域性符號的 _start。
.data
msg: .ascii "Hello World\n"
.text
.global _start
如 $build/usr/include/asm/unistd_32.h 中定義的那樣,write 和 _exit 的系統呼叫號為
#define __NR_exit 1
#define __NR_write 4
引數的傳遞方式與在 C 程式中一樣,使用正確的暫存器。設定好一切後,使用 int $0x80 進行系統呼叫。
_start:
movl $4, %eax ; use the `write` [interrupt-flavor] system call
movl $1, %ebx ; write to stdout
movl $msg, %ecx ; use string "Hello World"
movl $12, %edx ; write 12 characters
int $0x80 ; make system call
movl $1, %eax ; use the `_exit` [interrupt-flavor] system call
movl $0, %ebx ; error code 0
int $0x80 ; make system call
在 $build/usr/include/asm/unistd_64.h 中,系統呼叫號定義如下
#define __NR_write 1
#define __NR_exit 60
引數的傳遞方式與 int $0x80 示例中相同,只是暫存器的順序不同。系統呼叫使用 syscall 完成。
_start:
movq $1, %rax ; use the `write` [fast] syscall
movq $1, %rdi ; write to stdout
movq $msg, %rsi ; use string "Hello World"
movq $12, %rdx ; write 12 characters
syscall ; make syscall
movq $60, %rax ; use the `_exit` [fast] syscall
movq $0, %rdi ; error code 0
syscall ; make syscall
這是一個示例庫函式的 C 原型。
Window XCreateWindow(display, parent, x, y, width, height, border_width, depth,
class, visual, valuemask, attributes)
引數的傳遞方式與 int $0x80 示例中相同,只是暫存器的順序不同。
庫函式在原始檔開頭宣告(庫路徑在編譯連結時宣告)。
extern XCreateWindow
mov rdi, [xserver_pdisplay]
mov rsi, [xwin_parent]
mov rdx, [xwin_x]
mov rcx, [xwin_y]
mov r8, [xwin_width]
mov r9, [xwin_height]
mov rax, attributes
push rax ; ARG 12
sub rax, rax
mov eax, [xwin_valuemask]
push rax ; ARG 11
mov rax, [xwin_visual]
push rax ; ARG 10
mov rax, [xwin_class]
push rax ; ARG 9
mov rax, [xwin_depth]
push rax ; ARG 8
mov rax, [xwin_border_width]
push rax ; ARG 7
call XCreateWindow
mov [xwin_window], rax
注意函式的最後幾個引數,它們以相反的順序被壓入堆疊。