实现一个最简单的-X86_64-crt

2016-10-05 Mithrilwoodrat 更多博文 » 博客 » GitHub »

原文链接 http://woodrat.xyz/2016/10/05/%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E6%9C%80%E7%AE%80%E5%8D%95%E7%9A%84-X86_64-crt/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


之前闲着无聊,参考着《程序员的自我修养》把最后一章的 minicrt 移植到了 64 位环境下。

项目地址:https://github.com/Mithrilwoodrat/toy-crt

移植到64位下主要存在的问题就是 read write 等 system call 是由 gcc 内联汇编实现的,移植到64位下需要按照64位的格式重写。

64位与32位汇编的区别参考 csapp。具体来说有一下几点:

  • 使用 syscall 代替 int 0x80
  • system call table 与 32 位下不一致,如 $60 为 exit, $0 为 read, $1为 write
  • 64 位下参数传递的方式很多时候直接通过寄存器而非栈

具体64位汇编相关的部分可以参考 Say hello to x64 Assembly

还有就是, gcc 内联汇编为 AT&T 格式, 具体使用方法参见 GNU 的官方文档

下面结合代码讲解 c runtime 是做什么的,以及如何实现一个 c runtime。

我们先创建一个最简单的 c 文件如下

int main()
{
  return 0;
}

使用 gcc 编译后 readelf -S a.out

 45: 0000000000400530     2 FUNC    GLOBAL DEFAULT   11 __libc_csu_fini
    46: 0000000000601018     0 NOTYPE  WEAK   DEFAULT   22 data_start
    47: 0000000000601028     0 NOTYPE  GLOBAL DEFAULT   22 _edata
    48: 0000000000400534     0 FUNC    GLOBAL DEFAULT   12 _fini
    49: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@@GLIBC_
    50: 0000000000601018     0 NOTYPE  GLOBAL DEFAULT   22 __data_start
    51: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    52: 0000000000601020     0 OBJECT  GLOBAL HIDDEN    22 __dso_handle
    53: 0000000000400540     4 OBJECT  GLOBAL DEFAULT   13 _IO_stdin_used
    54: 00000000004004c0   101 FUNC    GLOBAL DEFAULT   11 __libc_csu_init
    55: 0000000000601030     0 NOTYPE  GLOBAL DEFAULT   23 _end

可以看到 gcc 自动加入了好几个函数,这些函数作用如下。

__libc_start_main 调用 __libc_csu_init 来进行初始化工作后 call main, 在 main 返回后调用 __libc_csu_fini 处理收尾工作。

readelf -s /usr/lib64/crt1.o                                                    

Symbol table '.symtab' contains 19 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    2 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    9 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT   10 
     9: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS init.c
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND __libc_csu_fini
    11: 0000000000000000    43 FUNC    GLOBAL DEFAULT    2 _start
    12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND __libc_csu_init
    13: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND main
    14: 0000000000000000     0 NOTYPE  WEAK   DEFAULT    7 data_start
    15: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    16: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    4 _IO_stdin_used
    17: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND __libc_start_main
    18: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT    7 __data_start

这几个函数定义于 /usr/lib64/crt1.o 中。 glibc 的入口为 _start 由 ld 脚本默认指定,可修改。 代码在 glibc/sysdeps/i386/Start.S 中,为平台相关,这里和书上一致摘取部分 i386 下的代码。

    leal __libc_csu_fini@GOTOFF(%ebx), %eax
    pushl %eax
    leal __libc_csu_init@GOTOFF(%ebx), %eax
    pushl %eax
    pushl %ecx      /* Push second argument: argv.  */
    pushl %esi      /* Push first argument: argc.  */
    pushl main@GOT(%ebx)
    /* Call the user's main function, and exit with its value.
       But let the libc call main.    */
    call __libc_start_main@PLT

可以看到 _start 主要工作为将几个函数和 argc argv 的地址传递给 __libc_start_main

__libc_start_main 的函数原型定义在 glibc\Csu\Libc-start.c 中如下

STATIC int LIBC_START_MAIN (int (*main) (int, char **, char **
                     MAIN_AUXVEC_DECL),
                int argc,
                char **argv,
#ifdef LIBC_START_MAIN_AUXVEC_ARG
                ElfW(auxv_t) *auxvec,
#endif
                __typeof (main) init,   /* main 调用前的初始化 */
                void (*fini) (void),     /* main 结束后的收尾 */ 
                void (*rtld_fini) (void),  /* 动态加载有关的收尾工作, rtld aka runtime loader */
                void *stack_end)  /* 标明栈底地址 */
     __attribute__ ((noreturn));
  /* Note: the fini parameter is ignored here for shared library.  It
   is registered with __cxa_atexit.  This had the disadvantage that
   finalizers were called in more than one place.  */

注释和之前对 init 和 finit 的说明一致。

在函数最后

result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
exit(result);

调用了 main,在取得返回值后调用 exit 退出。 exit 处理 atexit 后调用 _exit (exit syscall) 退出。

更多具体的代码就不一一列举了,自己去看 glibc 源码吧 : )

c runtime 的大致功能,结合上面的代码和《程序员的自我修养》 第 4 部分的内容,总结如下:

  • 初始化全局变量
  • 初始化堆
  • 初始化 I/O
  • 获取 argv 和 env
  • call main 并记录 ret
  • exit(ret)

了解了这些,我们可以试着绕过 glibc 直接用汇编写一个 hello world,看一下没有 glibc 的情况下应该怎么写。

hello.S

.data
hello:
    .string "Hello World!\n"

.text
.globl _start
_start:
    movq $1, %rax
    movq $1, %rdi
    movq $hello, %rsi
    movq $13, %rdx
    syscall

    movq $60, %rax
    movq $0, %rdi
    syscall
gcc -c -fno-builtin -nostdlib hello.S -o hello.o 
# gcc 这里也可以换成 as hello.S -o hello.o , 为了统一使用 gcc
ld -static -e _start hello.o -o hello
# 指定入口为自定义的 _start ,这个入口什么初始化都没做,直接开始执行。
./hello                                
Hello World!

ls 可以看到, 最后生成的的 ELF 可执行文件仅 920 字节。

$ file hello                              
hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

$ ls hello -l                             
-rwxr-xr-x 1 woodrat users 920 Oct  6 16:59 hello

看了上面这个汇编直接实现的 Hello World,我们要做的事情其实就是在这个的基础上增加一些功能。

hello.S 中的入口为 _start,什么都没做直接开始执行程序。而 crt 实现的入口函数还需要做上面提到的几个初始化工作,还有就是 crt 需要将汇编实现的 system call 包装成 C 语言函数的形式,在这个基础上一些基本的库函数就可以用纯 C 实现了。

根据上面总结的功能,我们可以开始实现一个最简单的 c runtime 了。既然是最简单的,大部分功能都可以略过,只要最后能运行就成。代码主要参照 《程序员的自我修养》 最后一章的 minicrt。

先写入口函数 crt_entry

void crt_entry(void) 
{
    int ret;
    int argc;
    char** argv;

    char * rbp_reg = 0;
    // on 64bit system use rbp instead of ebp
    //简单来说, gcc 内联汇编其实类似于 web 中的模板,第一个 `:` 后为输出, 第二个 `:` 
    //后为输入。
    asm volatile("movq %%rbp, %0 \n":"=m" (rbp_reg)); 
    argc = *(int *) (rbp_reg + 8);
    argv = (char **) (rbp_reg + 16);


    if ( !crt_heap_init()){
        die("heap init failed!");
    }

    if ( !crt_io_init()) {
        die("IO init failed!");
    }

    ret = main(argc, argv);
    _exit(ret);
}

这里略过的初始化全局变量和环境变量,仅初始化堆和 I/O, 获取 argc argv 后直接调用 main。 x86-64下的调用栈参考x86-64-architecture-guide.html,根据这个文档和glibc/Sysdeps/X86_64/start.S中的注释可以知道 rbp + 8 为 argc,rpb+16 开始为 argv。

这里的 _exit 直接退出不处理 atexit 函数。

void _exit(int status)
{
    __asm__("movq $60, %%rax \n\t"
            "movq %0, %%rdi \n\t"
            "syscall \n\t"
            "hlt \n\t"/* Crash if somehow `exit' does return.    */
            :: "g" (status)); /* input */
}

heap init 主要调用 brk (syscall $12) 申请固定大小内存,以双向链表方式维护,并暴露 malloc、free 接口给用户。

int crt_heap_init()
{
    void *base = NULL;
    heap_header *header = NULL;
    // 32 MB heap size
    size_t 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;
}

heap init 后 memory map 如下

  cat /proc/4680/maps                                                         
  00400000-00401000 r-xp 00000000 08:01 29099192                           /home/woodrat/Desktop/toy-crt/bin/test
  00601000-00602000 rw-p 00001000 08:01 29099192                           /home/woodrat/Desktop/toy-crt/bin/test
  01643000-03643000 rw-p 00000000 00:00 0                                  [heap]
  7ffcf89ce000-7ffcf89ef000 rw-p 00000000 00:00 0                          [stack]
  7ffcf89f5000-7ffcf89f7000 r--p 00000000 00:00 0                          [vvar]
  7ffcf89f7000-7ffcf89f9000 r-xp 00000000 00:00 0                          [vdso]
  ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

  heap field = 03643000 - 01643000  = 32M

io init 其实什么都没做,主要是在 stdio.c 中,wrap read write open 等 syscall。

int crt_io_init()
{
    return 1;
}

ps : 我偷懒没有写 open close 等,只实现了 read 和 write。

这些都准备好了以后,加上 stdio 以及 stdlib 中的一些常用函数就可以得到一个最简单的 crt 了。

测试用的 test.c 如下

#include "toy_crt.h"

static const char *str = "Hello World!";

void test_puts()
{
    puts(str);
}

void test_iota() 
{
    int len = strlen(str);
    char len_str[10];
    itoa(len, len_str);
    puts(len_str);
}

void test_malloc()
{
    int *p_int = (int *) malloc(sizeof(int));
    *p_int = 10;
    char len_str[10];
    itoa(*p_int, len_str);
    puts(len_str);
    free(p_int);
}

int main(int argc,char * argv[])
{
    test_puts();
    test_iota();
    test_malloc();
    puts("argc:");
    putchar(argc + '0');
    putchar('\n');
    puts("argv:");
    int i;
    for (i = 0; i < argc; i++) {
        puts(argv[i]);
    }
    getchar();
    return 42;
}

输出如下

bin/test 1 2 3 4                                                                                             
Hello World!
12
10
argc:
5
argv:
bin/test
1
2
3
4

readelf 可以看到,生成的文件里没有 __libc_start_main, 并且入口地址 0x40010d 为上面定义的 crt_entry(ld -e crt_entry 指定)。

readelf -h bin/test                    
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x40010d
  Start of program headers:          64 (bytes into file)
  Start of section headers:          6400 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         3
  Size of section headers:           64 (bytes)
  Number of section headers:         15
  Section header string table index: 12
readelf -s bin/test                     

Symbol table '.symtab' contains 42 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000004000e8     0 SECTION LOCAL  DEFAULT    1 
     2: 0000000000400892     0 SECTION LOCAL  DEFAULT    2 
     3: 00000000004008d0     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000601000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000601008     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT    9 
    10: 0000000000000000     0 SECTION LOCAL  DEFAULT   10 
    11: 0000000000000000     0 SECTION LOCAL  DEFAULT   11 
    12: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS entry.c
    13: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS test.c
    14: 0000000000601000     8 OBJECT  LOCAL  DEFAULT    4 str
    15: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS stdio.c
    16: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS string.c
    17: 00000000004003d0   127 FUNC    LOCAL  DEFAULT    1 reverse
    18: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS stdlib.c
    19: 00000000004007c1    39 FUNC    LOCAL  DEFAULT    1 brk
    20: 000000000040033b    38 FUNC    GLOBAL DEFAULT    1 putchar
    21: 00000000004007e8   170 FUNC    GLOBAL DEFAULT    1 crt_heap_init
    22: 000000000040010d   113 FUNC    GLOBAL DEFAULT    1 crt_entry
    23: 0000000000400361    62 FUNC    GLOBAL DEFAULT    1 puts
    24: 0000000000400670   337 FUNC    GLOBAL DEFAULT    1 malloc
    25: 000000000040044f   191 FUNC    GLOBAL DEFAULT    1 itoa
    26: 0000000000601008     8 OBJECT  GLOBAL DEFAULT    5 list_head
    27: 00000000004002db    48 FUNC    GLOBAL DEFAULT    1 write
    28: 000000000040030b    48 FUNC    GLOBAL DEFAULT    1 read
    29: 0000000000400196    22 FUNC    GLOBAL DEFAULT    1 test_puts
    30: 0000000000601008     0 NOTYPE  GLOBAL DEFAULT    5 __bss_start
    31: 0000000000400235   155 FUNC    GLOBAL DEFAULT    1 main
    32: 00000000004002d0    11 FUNC    GLOBAL DEFAULT    1 crt_io_init
    33: 000000000040039f    49 FUNC    GLOBAL DEFAULT    1 getchar
    34: 00000000004001e6    79 FUNC    GLOBAL DEFAULT    1 test_malloc
    35: 00000000004000e8    37 FUNC    GLOBAL DEFAULT    1 die
    36: 00000000004001ac    58 FUNC    GLOBAL DEFAULT    1 test_iota
    37: 0000000000601008     0 NOTYPE  GLOBAL DEFAULT    4 _edata
    38: 0000000000601010     0 NOTYPE  GLOBAL DEFAULT    5 _end
    39: 000000000040017e    24 FUNC    GLOBAL DEFAULT    1 _exit
    40: 000000000040050e    57 FUNC    GLOBAL DEFAULT    1 strlen
    41: 0000000000400547   297 FUNC    GLOBAL DEFAULT    1 free

具体的代码参考最上面给出的 github repo.