系统调用
Syscall简介
syscall 类似于一种全局可用的函数,由操作系统内核提供。系统调用会接收寄存器中的参数,并执行包含这些参数的函数。
Linux 内核提供了许多可用的系统调用。我们可以通过读取系统文件中的 unistd_64.h 字段来获取这些调用的列表以及每个调用的编号 syscall number 。
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里面是这样子的
int execve(const char *filename, char *const argv[], char *const envp[]);
对应syscall参数
| 参数 | 寄存器 | 含义 |
|---|---|---|
rax | 59 | syscall number |
rdi | filename | 要执行的程序路径 |
rsi | argv | 参数数组 |
rdx | envp | 环境变量数组 |
也就是近似这样:
mov rax, 59 ; execve
mov rdi, "/bin/sh" ; 第一个参数:程序路径
mov rsi, 0 ; 第二个参数:argv
mov rdx, 0 ; 第三个参数:envp
syscall
但是汇编里面不能直接把字符串直接赋值给指针,所以我们需要用push来压个栈;同时系统对系统调用时会以\x0为结尾,所以需要提前放0进入进去,构造一个/bin/sh\0的这么一个字符串
也就是这样
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 库函数,比如:printf、puts、scanf、malloc、strlen这些函数不是我们自己在当前 .s 文件里实现的,而是在外部库里,比如 libc 里面。
所以需要用:
extern printf
告诉汇编器:
printf 这个符号不是我当前文件里定义的,后面链接的时候,你去外部库里找。
例如:
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
这里:
extern printf
表示 printf 来自外部。
call printf
表示调用外部函数。
原则
Linux x64 外部函数调用参数
Linux x64 使用 System V ABI。
前 6 个参数通过寄存器传递:
第 1 个参数:rdi
第 2 个参数:rsi
第 3 个参数:rdx
第 4 个参数:rcx
第 5 个参数:r8
第 6 个参数:r9
比如 C 语言里:
printf("It's %s\n", "Aligned!");
汇编里就是:
mov rdi, fmt ; 第 1 个参数,格式字符串
mov rsi, msg ; 第 2 个参数,%s 对应的字符串
xor eax, eax ; printf 是可变参数函数,需要清空 eax
call printf
这里的:
xor eax, eax
是因为 printf 属于可变参数函数。
在 Linux x64 ABI 里,调用可变参数函数时,al 要表示用了几个向量寄存器传浮点参数。没有浮点参数时,直接置 0:
xor eax, eax
注意:字符串结尾要有 0
如果使用 %s,字符串必须以 0x00 结尾。
正确写法:
fmt db "It's %s", 10, 0msg db "Aligned!", 0
不要写成:
msg db "Aligned!", 10
因为 %s 会一直读,直到遇到 0x00 才停止。
如果没有 0x00,可能会读到后面的脏数据,甚至崩溃。
栈对齐
Linux x64 下,调用函数前有一个重要规则:执行 call 指令之前,rsp 必须是 16 字节对齐的。
所以我们需要检查rsp的值,如果遇到rsp不等于0的情况,就需要保证堆栈平衡
也就是经常需要补8的原因:
print:
sub rsp, 8
call printf
add rsp, 8
ret
常见的Libc函数
printf 函数
printf 是 libc 中最常见的输出函数,用来按照指定格式打印内容。
在 C 语言中:
printf("It's %s\n", "Aligned!");在 Linux x64 汇编中,前几个参数通过寄存器传递:
第 1 个参数:rdi
第 2 个参数:rsi
第 3 个参数:rdx
第 4 个参数:rcx
第 5 个参数:r8
第 6 个参数:r9所以:
printf("It's %s\n", "Aligned!");对应汇编:
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其中:
mov rdi, fmt相当于传入第一个参数:
printf(fmt, msg);mov rsi, msg相当于传入第二个参数:
printf("It's %s\n", msg);常见格式符
%d 十进制整数
%x 十六进制整数
%p 指针地址
%s 字符串
%c 字符例如:
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 语言:
printf("num = %d, str = %s\n", 123, "hello");输出:
num = 123, str = helloprintf 注意点
字符串必须以 0x00 结尾
错误写法:
msg db "hello"正确写法:
msg db "hello", 0因为 %s 会一直往后读,直到遇到 0x00 才停止。
printf 是可变参数函数
printf 的参数数量不固定,比如:
printf("%d\n", a);
printf("%d %d\n", a, b);
printf("%d %d %d\n", a, b, c);所以在 Linux x64 下,调用前一般要写:
xor eax, eax表示没有使用向量寄存器传递浮点参数。
初学阶段可以直接记:
调用 printf 前写 xor eax, eaxscanf 函数
scanf 是 libc 中常见的输入函数,用来从标准输入读取数据。
在 C 语言中:
scanf("%d", &num);注意,scanf 需要的是变量地址,不是变量值。
读取整数
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 语言:
int num;
scanf("%d", &num);
printf("num = %d\n", num);重点:scanf 传的是地址
这个很重要。
读取整数时:
mov rsi, num意思是把 num 的地址传给 scanf。
不要写成:
mov rsi, [num]因为:
mov rsi, num表示:
rsi = num 的地址而:
mov rsi, [num]表示:
rsi = num 里面存的值scanf 需要写入数据,所以必须给它一个可写地址。
scanf 读取字符串
C 语言:
char buf[64];
scanf("%63s", buf);汇编:
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这里推荐写:
inputFmt db "%63s", 0而不是:
inputFmt db "%s", 0因为 %s 不限制长度,容易造成缓冲区溢出。
如果 buf 是 64 字节,那么最多读 63 个字符,最后 1 个字节留给 0x00 结尾。
scanf 常见格式符
%d 读取十进制整数,需要 int *
%ld 读取 long,需要 long *
%x 读取十六进制整数
%s 读取字符串,需要 char *
%c 读取单个字符,需要 char *例如:
scanf("%d", &num);
scanf("%s", buf);
scanf("%c", &ch);汇编里本质都是:
第 1 个参数:格式字符串
第 2 个参数:变量地址scanf 返回值
scanf 的返回值在 rax / eax 中。
它返回成功读取了几个数据。
例如:
scanf("%d %d", &a, &b);如果两个整数都读取成功,返回值是:
2如果只成功读取一个,返回:
1如果失败,可能返回:
0所以汇编里可以这样判断:
call scanf
cmp eax, 1
jne input_errorscanf 注意点
1. 第二个参数一般是地址
读取整数:
mov rsi, num读取字符串:
mov rsi, buf不要把变量里面的值传过去。
2. %s 要限制长度
危险写法:
fmt db "%s", 0推荐写法:
fmt db "%63s", 0如果缓冲区是:
buf resb 64那格式符应该限制为:
"%63s"3. scanf 也是可变参数函数
所以调用前也建议写:
xor eax, eax和 printf 一样。