实现一个最简单的-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.