构建自己的运行库

本文详细介绍了如何构建一个名为minicrt的运行库,包括入口函数、堆管理、基本文件操作、字符串操作和格式化输出等功能。通过使用系统调用和自定义的数据结构,实现了malloc、free、fopen、fclose等基本功能,并提供了测试案例。minicrt简化了内存管理和I/O操作,便于理解程序的底层工作原理。
摘要由CSDN通过智能技术生成

如何构建自己的运行库

之前介绍了《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_crthello,在栈空间初始化之前分布如下所示

...
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_initmini_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;     
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫步旅人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值