系统调用

Syscall简介

syscall 类似于一种全局可用的函数,由操作系统内核提供。系统调用会接收寄存器中的参数,并执行包含这些参数的函数。

Linux 内核提供了许多可用的系统调用。我们可以通过读取系统文件中的 unistd_64.h 字段来获取这些调用的列表以及每个调用的编号 syscall number

1
2
3
4
5
6
7
8
9
10
cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h
#ifndef _ASM_X86_UNISTD_64_H
#define _ASM_X86_UNISTD_64_H 1

#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
#define __NR_stat 4
#define __NR_fstat 5

系统调用号

Linux x86-64 常见 syscall number:

syscall 编号 十六进制 作用
read 0 0x0 从文件描述符读取数据
write 1 0x1 向文件描述符写入数据
open 2 0x2 打开文件
close 3 0x3 关闭文件描述符
execve 59 0x3b 执行程序
exit 60 0x3c 退出进程
mmap 9 0x9 映射内存
mprotect 10 0xa 修改内存权限
socket 41 0x29 创建 socket
connect 42 0x2a 连接远程主机
dup2 33 0x21 复制文件描述符

比如说调用一个新的程序(such as sh):

在C里面是这样子的

1
int execve(const char *filename, char *const argv[], char *const envp[]);

对应syscall参数

参数 寄存器 含义
rax 59 syscall number
rdi filename 要执行的程序路径
rsi argv 参数数组
rdx envp 环境变量数组

也就是近似这样:

1
2
3
4
5
mov rax, 59        ; execve
mov rdi, "/bin/sh" ; 第一个参数:程序路径
mov rsi, 0 ; 第二个参数:argv
mov rdx, 0 ; 第三个参数:envp
syscall

但是汇编里面不能直接把字符串直接赋值给指针,所以我们需要用push来压个栈;同时系统对系统调用时会以\x0为结尾,所以需要提前放0进入进去,构造一个/bin/sh\0的这么一个字符串
也就是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xor rax,rax
push rax

move rbx,0x68732f6e69622f
push rbx

# 此时rsp就是这样:
# rsp -> 2f 62 69 6e 2f 73 68 00
# 00 00 00 00 00 00 00 00

mov rax, 59
mov rdi, rsp

xor rsi, rsi
xor rdx,rdx
syscall

外部函数调用

在汇编里调用 C 库函数,比如:printfputsscanfmallocstrlen这些函数不是我们自己在当前 .s 文件里实现的,而是在外部库里,比如 libc 里面。

所以需要用:

1
extern printf

告诉汇编器:

printf 这个符号不是我当前文件里定义的,后面链接的时候,你去外部库里找。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
global main
extern printf

section .data
fmt db "It's %s", 10, 0
msg db "Aligned!", 0

section .text
main:
sub rsp, 8

mov rdi, fmt
mov rsi, msg
xor eax, eax
call printf

add rsp, 8
xor eax, eax
ret

这里:

1
extern printf

表示 printf 来自外部。

1
call printf

表示调用外部函数。

原则

Linux x64 外部函数调用参数

Linux x64 使用 System V ABI。

前 6 个参数通过寄存器传递:

1
2
3
4
5
6
第 1 个参数:rdi
第 2 个参数:rsi
第 3 个参数:rdx
第 4 个参数:rcx
第 5 个参数:r8
第 6 个参数:r9

比如 C 语言里:

1
printf("It's %s\n", "Aligned!");

汇编里就是:

1
2
3
4
mov rdi, fmt      ; 第 1 个参数,格式字符串
mov rsi, msg ; 第 2 个参数,%s 对应的字符串
xor eax, eax ; printf 是可变参数函数,需要清空 eax
call printf

这里的:

1
xor eax, eax

是因为 printf 属于可变参数函数。
在 Linux x64 ABI 里,调用可变参数函数时,al 要表示用了几个向量寄存器传浮点参数。没有浮点参数时,直接置 0:

1
xor eax, eax

注意:字符串结尾要有 0

如果使用 %s,字符串必须以 0x00 结尾。

正确写法:

1
fmt db "It's %s", 10, 0msg db "Aligned!", 0

不要写成:

1
msg db "Aligned!", 10

因为 %s 会一直读,直到遇到 0x00 才停止。
如果没有 0x00,可能会读到后面的脏数据,甚至崩溃。

栈对齐

Linux x64 下,调用函数前有一个重要规则:执行 call 指令之前,rsp 必须是 16 字节对齐的。

所以我们需要检查rsp的值,如果遇到rsp不等于0的情况,就需要保证堆栈平衡

也就是经常需要补8的原因:

1
2
3
4
5
6
7
print:
sub rsp, 8

call printf

add rsp, 8
ret

常见的Libc函数

printf 函数

printf 是 libc 中最常见的输出函数,用来按照指定格式打印内容。

在 C 语言中:

1
printf("It's %s\n", "Aligned!");

在 Linux x64 汇编中,前几个参数通过寄存器传递:

1
2
3
4
5
6
第 1 个参数:rdi
第 2 个参数:rsi
第 3 个参数:rdx
第 4 个参数:rcx
第 5 个参数:r8
第 6 个参数:r9

所以:

1
printf("It's %s\n", "Aligned!");

对应汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extern printf

section .data
fmt db "It's %s", 10, 0
msg db "Aligned!", 0

section .text
; call printf 前要保证 rsp 16 字节对齐
sub rsp, 8

mov rdi, fmt ; 第 1 个参数:格式字符串
mov rsi, msg ; 第 2 个参数:%s 对应的字符串
xor eax, eax ; printf 是可变参数函数,al 置 0
call printf

add rsp, 8

其中:

1
mov rdi, fmt

相当于传入第一个参数:

1
printf(fmt, msg);
1
mov rsi, msg

相当于传入第二个参数:

1
printf("It's %s\n", msg);

常见格式符

1
2
3
4
5
%d    十进制整数
%x 十六进制整数
%p 指针地址
%s 字符串
%c 字符

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
section .data
fmt db "num = %d, str = %s", 10, 0
msg db "hello", 0

section .text
sub rsp, 8

mov rdi, fmt
mov rsi, 123
mov rdx, msg
xor eax, eax
call printf

add rsp, 8

对应 C 语言:

1
printf("num = %d, str = %s\n", 123, "hello");

输出:

1
num = 123, str = hello

printf 注意点

字符串必须以 0x00 结尾

错误写法:

1
msg db "hello"

正确写法:

1
msg db "hello", 0

因为 %s 会一直往后读,直到遇到 0x00 才停止。


printf 是可变参数函数

printf 的参数数量不固定,比如:

1
2
3
printf("%d\n", a);
printf("%d %d\n", a, b);
printf("%d %d %d\n", a, b, c);

所以在 Linux x64 下,调用前一般要写:

1
xor eax, eax

表示没有使用向量寄存器传递浮点参数。

初学阶段可以直接记:

1
调用 printf 前写 xor eax, eax

scanf 函数

scanf 是 libc 中常见的输入函数,用来从标准输入读取数据。

在 C 语言中:

1
scanf("%d", &num);

注意,scanf 需要的是变量地址,不是变量值。


读取整数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
global main
extern printf
extern scanf

section .data
inputFmt db "%d", 0
outputFmt db "num = %d", 10, 0

section .bss
num resd 1

section .text
main:
sub rsp, 8

; scanf("%d", &num)
mov rdi, inputFmt
mov rsi, num
xor eax, eax
call scanf

; printf("num = %d\n", num)
mov rdi, outputFmt
mov esi, [num]
xor eax, eax
call printf

add rsp, 8

xor eax, eax
ret

对应 C 语言:

1
2
3
int num;
scanf("%d", &num);
printf("num = %d\n", num);

重点:scanf 传的是地址

这个很重要。

读取整数时:

1
mov rsi, num

意思是把 num 的地址传给 scanf

不要写成:

1
mov rsi, [num]

因为:

1
mov rsi, num

表示:

1
rsi = num 的地址

而:

1
mov rsi, [num]

表示:

1
rsi = num 里面存的值

scanf 需要写入数据,所以必须给它一个可写地址。


scanf 读取字符串

C 语言:

1
2
char buf[64];
scanf("%63s", buf);

汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
global main
extern printf
extern scanf

section .data
inputFmt db "%63s", 0
outputFmt db "you input: %s", 10, 0

section .bss
buf resb 64

section .text
main:
sub rsp, 8

; scanf("%63s", buf)
mov rdi, inputFmt
mov rsi, buf
xor eax, eax
call scanf

; printf("you input: %s\n", buf)
mov rdi, outputFmt
mov rsi, buf
xor eax, eax
call printf

add rsp, 8

xor eax, eax
ret

这里推荐写:

1
inputFmt db "%63s", 0

而不是:

1
inputFmt db "%s", 0

因为 %s 不限制长度,容易造成缓冲区溢出。

如果 buf 是 64 字节,那么最多读 63 个字符,最后 1 个字节留给 0x00 结尾。


scanf 常见格式符

1
2
3
4
5
%d    读取十进制整数,需要 int *
%ld 读取 long,需要 long *
%x 读取十六进制整数
%s 读取字符串,需要 char *
%c 读取单个字符,需要 char *

例如:

1
2
3
scanf("%d", &num);
scanf("%s", buf);
scanf("%c", &ch);

汇编里本质都是:

1
2
第 1 个参数:格式字符串
第 2 个参数:变量地址

scanf 返回值

scanf 的返回值在 rax / eax 中。

它返回成功读取了几个数据。

例如:

1
scanf("%d %d", &a, &b);

如果两个整数都读取成功,返回值是:

1
2

如果只成功读取一个,返回:

1
1

如果失败,可能返回:

1
0

所以汇编里可以这样判断:

1
2
3
call scanf
cmp eax, 1
jne input_error

scanf 注意点

1. 第二个参数一般是地址

读取整数:

1
mov rsi, num

读取字符串:

1
mov rsi, buf

不要把变量里面的值传过去。


2. %s 要限制长度

危险写法:

1
fmt db "%s", 0

推荐写法:

1
fmt db "%63s", 0

如果缓冲区是:

1
buf resb 64

那格式符应该限制为:

1
"%63s"

3. scanf 也是可变参数函数

所以调用前也建议写:

1
xor eax, eax

printf 一样。