sys_brk(即调用号为12的brk系统调用)是Linux中一个用于动态内存管理的核心系统调用。它直接操作进程的程序中断点(program break),即堆(heap)的结束地址。通过改变这个中断点的位置,程序可以动态地增加或减少其数据段的大小。尽管现代C库(如glibc)通常使用mmap来实现更灵活的内存分配(如malloc),但在底层,sys_brk仍然是扩展堆空间的基本机制。了解sys_brk有助于理解进程地址空间的布局,以及操作系统如何为程序提供可变大小的内存区域。
sys_brk函数原型
1
int brk(void *addr);
参数
addr: 新的程序中断点地址。如果该地址比当前中断点高,则扩大堆;如果比当前中断点低,则缩小堆。如果传入0,系统调用会返回当前程序中断点的地址。
返回值
- 成功时返回新程序中断点地址(即参数
addr)。但对于现代x86_64 Linux,为了与旧版内核兼容,返回值在rax中会是0,失败时会是1。 - 失败时返回
-1,并设置errno(如ENOMEM)。
- 成功时返回新程序中断点地址(即参数
用例
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67.equ SYS_BRK, 12
.equ SYS_WRITE, 1
.equ SYS_EXIT, 60
.equ STDOUT, 1
.equ PAGE_SIZE, 4096
.section .data
initial_brk:
.quad 0x0
message:
.ascii "New memory allocated with sys_brk.\n\0"
msg_len = . - message
.section .text
.globl _start
_start:
# 获取当前程序中断点地址
mov $SYS_BRK, %rax
xor %rdi, %rdi
syscall
# 检查返回值以确保成功
cmp $0, %rax
je exit_error
# 将当前中断点地址保存起来
mov %rax, initial_brk(%rip)
# 计算新的中断点地址(增加一个页大小)
add $PAGE_SIZE, %rax
# 调用 sys_brk 扩展堆
mov $SYS_BRK, %rax
mov initial_brk(%rip), %rdi
add $PAGE_SIZE, %rdi
syscall
# 检查返回值,如果失败则退出
cmp $0, %rax
jne exit_error
# 将消息复制到新分配的内存区域
mov initial_brk(%rip), %rdi
mov $message, %rsi
mov $msg_len, %rcx
cld
rep movsb
# 使用 sys_write 打印新内存中的消息
mov $SYS_WRITE, %rax
mov $STDOUT, %rdi
mov initial_brk(%rip), %rsi
mov $msg_len, %rdx
syscall
# 退出程序
exit_success:
mov $SYS_EXIT, %rax
xor %rdi, %rdi
syscall
exit_error:
mov $SYS_EXIT, %rax
mov $1, %rdi
syscall分析:
上述代码演示了如何使用
sys_brk系统调用来动态地扩大堆内存。首先,程序通过调用
brk(0)来获取当前的程序中断点地址。在汇编中,这是通过将系统调用号12放入%rax并将%rdi设置为0来完成的。返回的地址会保存在%rax中,程序将其存储在initial_brk变量中。接着,程序将
initial_brk的值增加一个页面大小(4096字节),得到新的中断点地址。然后,它再次调用sys_brk,并将这个新的地址作为参数传递。如果调用成功,内核会调整堆的大小,使程序可以使用这块新分配的内存。最后,程序将一个字符串消息复制到这个新分配的内存区域,并使用
sys_write系统调用将其打印到标准输出。这证明了通过sys_brk分配的内存现在是可访问和可写入的。