Skip to content
New issue

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 #24

Open
chenpengcong opened this issue Nov 7, 2018 · 0 comments
Open

实现一个简单的CRT #24

chenpengcong opened this issue Nov 7, 2018 · 0 comments

Comments

@chenpengcong
Copy link
Owner

chenpengcong commented Nov 7, 2018

本文介绍如何实现自己的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 | 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

接下来在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 

编译选项

  • -fno-builtin参数关闭gcc的内置函数功能, 默认情况下gcc会把strlen, strcmp, exit等常用函数展开它内部的实现
  • -nostdlib表示不使用来自glibc, gcc的库文件和启动文件

接下来编写测试程序去使用这个库而不是默认的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函数

至此, 一个简单的C语言运行时库就完成了, 下面讲解编写该crt所遇到的坑

函数read, write的中的变量ret类型不能为int

比如我在read函数中将变量ret定义为int, 会导致错误Segmentation fault (core dumped)

使用objdump反编译查看汇编代码,比较ret为long和int情况有什么不同

  # 这是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调试证明堆栈确实被破坏了

(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,下面图示有助于理解是怎么被覆盖的

crt01

最后返回到main函数后堆栈基地址是错误的,那么使用该错误地址+偏移量去访问结果是未定义的

References

@chenpengcong chenpengcong changed the title 实现一个简单的CRT.md 实现一个简单的CRT Feb 7, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant