文章目录
如何构建自己的运行库
之前介绍了《main函数之前后》,这次,我们试图来构建一个自己的运行库。
本篇文章中的例子,来自于俞甲子、石凡、潘爱民的《程序员的自我修养-链接、装载与库》,对例子进行了更新,原书中是32位,这里是64位。也感谢这本书给我带来的帮助,对编译过程和程序的底层知识有了深一层的认识,而这篇文章也作为我的一个笔记和学习成果吧。
欢迎大家查看我的个人博客 (http://blog.wuzhenyu.com.cn)
本文中的源代码地址 https://github.com/small-cat/myCode_repository/tree/master/minicrt/c
废话不多说,我们实现的这个运行库,也叫作 minicrt。之前说过,main函数之前,入口函数需要完成各种初始化和准备工作,然后调用main主体函数,main函数结束时,再调用exit负责后续的清理工作。那么先确定我们这个minicrt的基本功能:
-
具有自己的入口函数
mini_crt_entry
-
基本的进程退出相关操作 exit
-
支持堆操作 malloc、free
-
支持基本的文件操作 fopen, fwrite. fclose, fread, fseek
-
支持基本的字符串操作 strcpy, strcmp, strlen
-
支持基本的字符串格式化和输出操作 printf sprintf
-
支持 atexit() 函数
简单起见,所有的申明都放在同一个头文件中 minicrt.h
入口函数
入口函数名为mini_crt_entry
,没有参数,也没有返回值,因为exit函数调用的时候,如果正常,函数会直接退出,不会回到入口函数继续执行并返回结果。同时,函数体内还需准备好程序运行的环境,包括main函数的命令行参数,初始化运行库,如堆、I/O等,结束部分主要负责清理程序运行资源。
main函数的两个参数为 argc, argv,argc是参数个数,argv是一个字符串数组,保存的是所有的命令参数。当进程被初始化时,它的堆栈中就保存着环境变量和传递给main函数的参数。汇编指令中,一般函数栈的开头都如下
push %rbp
mov %rsp,%rbp
sub $0x20,%rsp
这样,将基址寄存器rbp保存下来,然后开辟了一个32字节的栈空间作为函数栈空间。所以说,栈顶寄存器 rsp 指向的位置,是即将初始化的栈空间的顶部,即 rbp 指向的位置。如果我们像下面这样执行函数
mini_crt hello
命令行参数就是两个,mini_crt
和 hello
,在栈空间初始化之前分布如下所示
...
rsp指向的位置
2
argv[0]'s addr
argv[1]'s addr
...
地址从上往下是递增的,因为栈是往地址小的方向增长的
栈空间初始化之后,push rbp
,然后mov rsp, rbp
,此时,rbp 的值就成了之前的 rsp,也就是说,rbp+8的值就是2,rbp+16的值就是argv的首地址了(我的环境是64位elementary os)。
完成了获取命令行参数的代码后,还需要在入口函数体内实现对堆和 I/O 的初始化,分别申明为 mini_crt_heap_init
和 mini_crt_io_init
。然后调用main主体函数,main函数返回时,调用exit函数退出。exit函数完成两个任务,一个是调用由 atexit() 函数注册的退出回调函数,另一个就是结束进程。入口函数代码如下
void mini_crt_entry(void) {
int ret;
int argc;
char** argv;
char* ebp_reg;
//ebp_reg = %ebp
asm(
"mov %%rbp, %0 \t\n"
:"=r"(ebp_reg)
);
// 64bit, the size of rbp is 8 bytes.
argc = *(long *)(ebp_reg + 8);
argv = (char**)(ebp_reg + 16);
if (!mini_crt_heap_init()) {
crt_fatal_error("heap initialize failed.");
}
if (!mini_crt_io_init()) {
crt_fatal_error("IO initialize failed.");
}
// call main functions, and deliver the command line args.
ret = main(argc, argv);
exit(ret);
}
/* system call number of sys_exit is 60 */
void exit(int exitCode) {
asm(
"mov $0x3c, %%rax \n\t"
"mov %0, %%rdi \n\t"
"syscall \n\t"
::"m"(exitCode)
);
}
exit 函数,使用系统调用退出,64位系统调用,统一使用 syscall
,不是32位的 int 0x08h
。64位的系统调用号也与32位不同,sys_exit
的系统调用号为60,将 rax 寄存器的值设置为 60,rdi 为返回值,syscall 调用系统调用。
堆的实现
堆是一块巨大的内存空间,在这部分空间内,程序可以请求一块连续的内存并自由的使用,这块内存在程序主动放弃之前都会一直保持。如果进程的内存管理由操作系统的内核来做,那么就是说,每次程序申请堆空间,操作系统都要调用系统调用分配一块足够大的内存,给用户程序,从用户态切换到内核态,再切换到用户态,这样非常影响程序的性能。比较好的做法就是程序直接向操作系统一次申请一块适当大的空间,然后由程序自己管理这部分空间,当需要申请内存的时候,程序就从这块空间中切分一块,如果释放,就合并到这块空间中。所以,一般管理对空间分配的都是程序的运行库。
linux 提供了两个系统调用,brk/sbrk 和 mmap 来管理堆空间。在运行库中,有两种最基本的方法来管理堆空间的分配,一个是空闲链表法,一个是位图法。
空闲链表法,是将堆中各个空闲块按照链表的方式连接起来,链表采用双向链表的方式,当程序申请空间时,从前往后遍历链表,找到一个合适大小的块分配给程序,当释放空间时,将这块不再使用的空间加入到链表中,然后查看前后是否也是空闲块,如果是,将空闲块合并成一块,减少空间碎片化。当然,实际堆管理比这复杂的多,这只是简单说明一下原理。
位图法,是将整个空间划分成大量大小相等的块,用户请求内存的时候,分配整数个数的块给用户。第一块成为头Head,其余成为主体Body,未使用的为Free,所有使用两位即可表示一个块的使用情况,使用一个整数数组就能记录块的使用情况。
这里我们采用双链表的方式,来管理堆空间分配。
实现
-
采用空闲链表法管理堆分配
-
堆大小固定为 32MB,然后在这 32MB 中进行空间管理。(仅学习demo使用,尽量简单)
-
使用 brk 系统调用获取 32MB 空间
注意,由 brk/sbrk 分配的空间,仅仅只是虚拟地址空间,一开始是不会分配物理内存的,只有当进程试图访问某一个地址的时候,操作系统检测到访问异常,然后为被访问地址所在的页分配物理内存页
先确定链表的结构体
typedef struct _heap_header {
enum {
HEAP_BLOCK_FREE = 0xABABABAB, //magic number of free block
HEAP_BLOCK_USED = 0xCDCDCDCD //magic number of used block
}type;
unsigned size;
struct _heap_header* next;
struct _heap_header* prev;
} heap_header;
#define ADDR_ADD(a, o) (((char*)(a)) + o)
#define HEADER_SIZE (sizeof(heap_header))
结构体type表示块的状态,是否使用,size为块的大小,next 和 prev 表示双链表节点向前和向后的指针。宏函数 ADDR_ADD(a, o)
获取结构体的实际使用内存地址。o 表示 HEADER_SIZE
时,指针往后偏移,跳过结构体节点的头部,后面的空间就是能够供程序直接使用的空间大小。
brk函数通过 sys_brk
系统调用来实现
static int brk(void* end_data_segment) {
int ret = 0;
// Linux brk system call
// sys_brk system call number: 12
// rax:12, rdi:end_data_segment
asm (
"mov $12, %%rax \n\t"
"mov %1, %%rdi \n\t"
"syscall \n\t"
:"=r"(ret)
:"b"(end_data_segment)
);
return ret;
}
int mini_crt_heap_init() {
void* base = NULL;
heap_header* header = NULL;
// 32MB heap size
unsigned heap_size = 1024 * 1024 * 32;
base = (void*)brk(0);
void* end = ADDR_ADD(base, heap_size);
end = (void*)brk(end);
if (!end) {
return 0;
}
header = (heap_header*)base;
header->size = heap_size;
header->type = HEAP_BLOCK_FREE;
header->next = NULL;
header->prev = NULL;
list_head = header;
return 1;
}
mini_crt_heap_init
函数中,通过 brk 函数申请了32MB的空间,同时初始化和加入空闲链表作为第一个链表节点。
void* malloc(unsigned size) {
heap_header* header;
if (0 == size) {
return NULL;
}
header = list_head;