linux汇编编程
系统调用
Syscall简介
syscall 类似于一种全局可用的函数,由操作系统内核提供。系统调用会接收寄存器中的参数,并执行包含这些参数的函数。
Linux 内核提供了许多可用的系统调用。我们可以通过读取系统文件中的 unistd_64.h 字段来获取这些调用的列表以及每个调用的编号 syscall number 。1
2
3
4
5
6
7
8
9
10cat /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
5mov 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
16xor 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 里面。
所以需要用:
1 | extern printf |
告诉汇编器:
printf 这个符号不是我当前文件里定义的,后面链接的时候,你去外部库里找。
例如:
1 | global main |
这里:
1 | extern printf |
表示 printf 来自外部。
1 | call printf |
表示调用外部函数。
原则
Linux x64 外部函数调用参数
Linux x64 使用 System V ABI。
前 6 个参数通过寄存器传递:
1 | 第 1 个参数:rdi |
比如 C 语言里:
1 | printf("It's %s\n", "Aligned!"); |
汇编里就是:
1 | mov rdi, fmt ; 第 1 个参数,格式字符串 |
这里的:
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
7print:
sub rsp, 8
call printf
add rsp, 8
ret
常见的Libc函数
printf 函数
printf 是 libc 中最常见的输出函数,用来按照指定格式打印内容。
在 C 语言中:
1 | printf("It's %s\n", "Aligned!"); |
在 Linux x64 汇编中,前几个参数通过寄存器传递:
1 | 第 1 个参数:rdi |
所以:
1 | printf("It's %s\n", "Aligned!"); |
对应汇编:
1 | extern printf |
其中:
1 | mov rdi, fmt |
相当于传入第一个参数:
1 | printf(fmt, msg); |
1 | mov rsi, msg |
相当于传入第二个参数:
1 | printf("It's %s\n", msg); |
常见格式符
1 | %d 十进制整数 |
例如:
1 | section .data |
对应 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 | printf("%d\n", a); |
所以在 Linux x64 下,调用前一般要写:
1 | xor eax, eax |
表示没有使用向量寄存器传递浮点参数。
初学阶段可以直接记:
1 | 调用 printf 前写 xor eax, eax |
scanf 函数
scanf 是 libc 中常见的输入函数,用来从标准输入读取数据。
在 C 语言中:
1 | scanf("%d", &num); |
注意,scanf 需要的是变量地址,不是变量值。
读取整数
1 | global main |
对应 C 语言:
1 | int 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 | char buf[64]; |
汇编:
1 | global main |
这里推荐写:
1 | inputFmt db "%63s", 0 |
而不是:
1 | inputFmt db "%s", 0 |
因为 %s 不限制长度,容易造成缓冲区溢出。
如果 buf 是 64 字节,那么最多读 63 个字符,最后 1 个字节留给 0x00 结尾。
scanf 常见格式符
1 | %d 读取十进制整数,需要 int * |
例如:
1 | scanf("%d", &num); |
汇编里本质都是:
1 | 第 1 个参数:格式字符串 |
scanf 返回值
scanf 的返回值在 rax / eax 中。
它返回成功读取了几个数据。
例如:
1 | scanf("%d %d", &a, &b); |
如果两个整数都读取成功,返回值是:
1 | 2 |
如果只成功读取一个,返回:
1 | 1 |
如果失败,可能返回:
1 | 0 |
所以汇编里可以这样判断:
1 | call scanf |
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 一样。
