We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
本文介绍如何实现自己的CRT(c runtime library)来完成标准库glibc的部分工作,该CRT只提供了read, write, close, exit这几个系统调用,完整代码在mini_crt
本文所编写的代码在下面环境下测试通过
$ cat /etc/issue Ubuntu 18.04.1 LTS $ uname -a Linux c1 4.15.0-36-generic #39-Ubuntu SMP Mon Sep 24 16:19:09 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux $ gcc --version gcc (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0 $ ld --version GNU ld (GNU Binutils for Ubuntu) 2.30
首先,我们需要实现一个入口函数, 它需要取得argc和argv, 然后调用main函数, 最终调用exit系统调用退出程序
我们平时编写的依赖于glibc的EELF可执行程序默认入口是_start, 定义在glibc/sysdeps/x86_64/start.S, 它是通过在链接控制脚本中指定的,可以通过$ ld -verbose查看ld使用的链接控制脚本
$ ld -verbose
$ ld -verbose | grep ENTRY ENTRY(_start)
_start函数除了上面描述的入口函数的功能还做了许多工作,这里不细究,部分内容可以参考我的另一篇文章glibc的构造函数和析构函数
实现的入口函数如下(entry.c)
#include "util.h" extern int main(int argc, char **argv); void mini_crt_entry() { int argc; char **argv; int status = 42; char* rbp_reg = 0; asm("mov %%rbp, %0 \n\t":"=m"(rbp_reg):); argc = *((int *)(rbp_reg + 8)); argv = (char **)(rbp_reg + 16); status = main(argc, argv); exit(status); }
argc和argv是从当前栈顶往高地址方向偏移8字节和16字节拿到的
为什么从堆栈中取得这argc和argv呢? 因为当ELF加载器(ELF Loader)将ELF文件加载到内存后,会初始化进程堆栈,包括argc, argv等, 所以在程序进入mini_crt_entry前, 进程堆栈中已经存放了argc和argv了,如下表所示,。进程堆栈初始化的细节可以看我的另一篇文章进程堆栈初始化
position content size(bytes) + comment ------------------------------------------------------------------------------------------------- stack pointer -> [argc = number of args] 8 [argv[0](pointer)] 8 [argv[1](pointer)] 8 [argv[...](pointer)] 8 * x [argv[n-1](pointer)] 8 [argv[n](pointer)] 8 (=NULL)
为什么是从偏移8字节开始取得呢? 因为函数执行的第一条指令就是保存堆栈基地址: push %rbp(重新强调下本文代码是基于64位机器编写的,因此是rbp寄存器), 栈增长了8字节, 所以我们需要从偏移8字节位置拿到argc
push %rbp
接下来在util.c中实现read, write, close和exit函数去调用真正的系统调用, 由于是在64位系统下编写, 所以使用了syscall来调用系统调用, 看懂下面代码需要了解Extended Asm(扩展汇编)的知识
#include "util.h" long read(int fd, void *buf, long count) { long ret; asm("movq $0, %%rax \n\t" "movq %1, %%rdi \n\t" "movq %2, %%rsi \n\t" "movq %3, %%rdx \n\t" "syscall \n\t" "movq %%rax, %0 \n\t" :"=m"(ret) :"m"(fd),"m"(buf),"m"(count) :"rax", "rdi", "rsi", "rdx"); return ret; } long write(int fd, const void *buf, long count) { long ret; asm("movq $1, %%rax \n\t" "movq %1, %%rdi \n\t" "movq %2, %%rsi \n\t" "movq %3, %%rdx \n\t" "syscall \n\t" "movq %%rax, %0 \n\t" :"=m"(ret) :"m"(fd),"m"(buf),"m"(count) :"rax", "rdi", "rsi", "rdx"); return ret; } int open(const char *pathname, int flags, int mode) { long fd; asm("movq $2, %%rax \n\t" "movq %1, %%rdi \n\t" "movq %2, %%rsi \n\t" "movq %3, %%rdx \n\t" "syscall \n\t" "movq %%rax, %0 \n\t" :"=m"(fd) :"m"(pathname),"m"(flags),"m"(mode) :"rax", "rdi", "rsi", "rdx"); return fd; } int close(int fd) { long ret; asm("movq $3, %%rax \n\t" "movq %1, %%rdi \n\t" "syscall \n\t" "movq %%rax, %0 \n\t" :"=m"(ret) :"m"(fd) :"rax", "rdi"); return ret; } void exit(int status) { asm("movq $60, %%rax \n\t" "movq %0, %%rdi \n\t" "syscall \n\t" : :"m"(status)); }
util.h代码如下
#define O_RDONLY 00 #define O_WRONLY 01 #define O_RDWR 02 long read(int fd, void *buf, long count); long write(int fd, const void *buf, long count); int open(const char *pathname, int flags, int mode); int close(int fd); void exit(int status);
到这里一个简单的crt就完成了, 将代码编译成静态库
$ gcc -c -fno-builtin -nostdlib entry.c util.c $ ar -rs minicrt.a util.o entry.o
编译选项
接下来编写测试程序去使用这个库而不是默认的glibc, 该程序从命令行参数读取文件名, 然后根据文件名读取1KB内容输出到标准输出(文件描述符0)
#include "util.h" int main(int argc, char **argv) { int fd; long count = 0; char buf[1024] = {0}; if (argc == 2) { fd = open(argv[1], O_RDONLY, 0); count = read(fd, buf, 1024); write(1, buf, count); close(fd); } return 0; }
编译, 链接, 运行如下
$ gcc -c -fno-builtin -nostdlib test.c $ ld -static -e mini_crt_entry test.o minicrt.a -o test $ ./test test.c #include "util.h" int main(int argc, char **argv) { int fd; long count = 0; char buf[1024] = {0}; if (argc == 2) { fd = open(argv[1], O_RDONLY, 0); count = read(fd, buf, 1024); write(1, buf, count); close(fd); } return 0; }
其中链接选项-e mini_crt_entry指定程序入口为mini_crt_entry函数
-e mini_crt_entry
至此, 一个简单的C语言运行时库就完成了, 下面讲解编写该crt所遇到的坑
函数read, write的中的变量ret类型不能为int
比如我在read函数中将变量ret定义为int, 会导致错误Segmentation fault (core dumped)
Segmentation fault (core dumped)
使用objdump反编译查看汇编代码,比较ret为long和int情况有什么不同
objdump
# 这是ret为long的汇编代码 0000000000400159 <read>: 400159: 55 push %rbp 40015a: 48 89 e5 mov %rsp,%rbp 40015d: 89 7d ec mov %edi,-0x14(%rbp) 400160: 48 89 75 e0 mov %rsi,-0x20(%rbp) 400164: 48 89 55 d8 mov %rdx,-0x28(%rbp) 400168: 48 c7 c0 00 00 00 00 mov $0x0,%rax 40016f: 48 8b 7d ec mov -0x14(%rbp),%rdi 400173: 48 8b 75 e0 mov -0x20(%rbp),%rsi 400177: 48 8b 55 d8 mov -0x28(%rbp),%rdx 40017b: 0f 05 syscall 40017d: 48 89 45 f8 mov %rax,-0x8(%rbp) 400181: 48 8b 45 f8 mov -0x8(%rbp),%rax 400185: 5d pop %rbp 400186: c3 retq # 这是ret为int的汇编代码 0000000000400159 <read>: 400159: 55 push %rbp 40015a: 48 89 e5 mov %rsp,%rbp 40015d: 89 7d ec mov %edi,-0x14(%rbp) 400160: 48 89 75 e0 mov %rsi,-0x20(%rbp) 400164: 48 89 55 d8 mov %rdx,-0x28(%rbp) 400168: 48 c7 c0 00 00 00 00 mov $0x0,%rax 40016f: 48 8b 7d ec mov -0x14(%rbp),%rdi 400173: 48 8b 75 e0 mov -0x20(%rbp),%rsi 400177: 48 8b 55 d8 mov -0x28(%rbp),%rdx 40017b: 0f 05 syscall 40017d: 48 89 45 fc mov %rax,-0x4(%rbp) 400181: 8b 45 fc mov -0x4(%rbp),%eax 400184: 48 98 cltq 400186: 5d pop %rbp 400187: c3 retq
区别在于40017d处的指令,mov %rax,-0x8(%rbp)和mov %rax,-0x4(%rbp),为什么右边这条指令会导致崩溃,原因在于该指令使用的偏移量有问题,导致指令push %rbp在栈中保存的值(main函数的堆栈基地址)被覆盖。下面我使用编译选项-g,然后用gdb调试证明堆栈确实被破坏了
mov %rax,-0x8(%rbp)
mov %rax,-0x4(%rbp)
(gdb) disas Dump of assembler code for function read: => 0x0000000000400159 <+0>: push %rbp 0x000000000040015a <+1>: mov %rsp,%rbp 0x000000000040015d <+4>: mov %edi,-0x14(%rbp) 0x0000000000400160 <+7>: mov %rsi,-0x20(%rbp) 0x0000000000400164 <+11>: mov %rdx,-0x28(%rbp) 0x0000000000400168 <+15>: mov $0x0,%rax 0x000000000040016f <+22>: mov -0x14(%rbp),%rdi 0x0000000000400173 <+26>: mov -0x20(%rbp),%rsi 0x0000000000400177 <+30>: mov -0x28(%rbp),%rdx 0x000000000040017b <+34>: syscall 0x000000000040017d <+36>: mov %rax,-0x4(%rbp) 0x0000000000400181 <+40>: mov -0x4(%rbp),%eax 0x0000000000400184 <+43>: cltq 0x0000000000400186 <+45>: pop %rbp 0x0000000000400187 <+46>: retq End of assembler dump. (gdb) p $rbp $1 = (void *) 0x7fffffffe418 (gdb) b *read+46 Breakpoint 3 at 0x400187: file util.c, line 15. (gdb) c Continuing. Breakpoint 3, 0x0000000000400187 in read (fd=3, buf=0x7fffffffe008, count=1024) at util.c:15 15 } (gdb) p $rbp $2 = (void *) 0x7fff00000000
可以看到read函数一开始保存的rbp寄存器的值为0x7fffffffe418,当函数准备返回时,rbp寄存器的值却变成了0x7fff00000000,下面图示有助于理解是怎么被覆盖的
最后返回到main函数后堆栈基地址是错误的,那么使用该错误地址+偏移量去访问结果是未定义的
References
The text was updated successfully, but these errors were encountered:
No branches or pull requests
本文介绍如何实现自己的CRT(c runtime library)来完成标准库glibc的部分工作,该CRT只提供了read, write, close, exit这几个系统调用,完整代码在mini_crt
本文所编写的代码在下面环境下测试通过
首先,我们需要实现一个入口函数, 它需要取得argc和argv, 然后调用main函数, 最终调用exit系统调用退出程序
我们平时编写的依赖于glibc的EELF可执行程序默认入口是_start, 定义在glibc/sysdeps/x86_64/start.S, 它是通过在链接控制脚本中指定的,可以通过
$ ld -verbose
查看ld使用的链接控制脚本_start函数除了上面描述的入口函数的功能还做了许多工作,这里不细究,部分内容可以参考我的另一篇文章glibc的构造函数和析构函数
实现的入口函数如下(entry.c)
argc和argv是从当前栈顶往高地址方向偏移8字节和16字节拿到的
为什么从堆栈中取得这argc和argv呢?
因为当ELF加载器(ELF Loader)将ELF文件加载到内存后,会初始化进程堆栈,包括argc, argv等, 所以在程序进入mini_crt_entry前, 进程堆栈中已经存放了argc和argv了,如下表所示,。进程堆栈初始化的细节可以看我的另一篇文章进程堆栈初始化
为什么是从偏移8字节开始取得呢?
因为函数执行的第一条指令就是保存堆栈基地址:
push %rbp
(重新强调下本文代码是基于64位机器编写的,因此是rbp寄存器), 栈增长了8字节, 所以我们需要从偏移8字节位置拿到argc接下来在util.c中实现read, write, close和exit函数去调用真正的系统调用, 由于是在64位系统下编写, 所以使用了syscall来调用系统调用, 看懂下面代码需要了解Extended Asm(扩展汇编)的知识
util.h代码如下
到这里一个简单的crt就完成了, 将代码编译成静态库
编译选项
接下来编写测试程序去使用这个库而不是默认的glibc, 该程序从命令行参数读取文件名, 然后根据文件名读取1KB内容输出到标准输出(文件描述符0)
编译, 链接, 运行如下
其中链接选项
-e mini_crt_entry
指定程序入口为mini_crt_entry函数至此, 一个简单的C语言运行时库就完成了, 下面讲解编写该crt所遇到的坑
函数read, write的中的变量ret类型不能为int
比如我在read函数中将变量ret定义为int, 会导致错误
Segmentation fault (core dumped)
使用
objdump
反编译查看汇编代码,比较ret为long和int情况有什么不同区别在于40017d处的指令,
mov %rax,-0x8(%rbp)
和mov %rax,-0x4(%rbp)
,为什么右边这条指令会导致崩溃,原因在于该指令使用的偏移量有问题,导致指令push %rbp
在栈中保存的值(main函数的堆栈基地址)被覆盖。下面我使用编译选项-g,然后用gdb调试证明堆栈确实被破坏了可以看到read函数一开始保存的rbp寄存器的值为0x7fffffffe418,当函数准备返回时,rbp寄存器的值却变成了0x7fff00000000,下面图示有助于理解是怎么被覆盖的
最后返回到main函数后堆栈基地址是错误的,那么使用该错误地址+偏移量去访问结果是未定义的
References
The text was updated successfully, but these errors were encountered: