从一个ELF程序的加载窥探操作系统内核-(5)
操作系统加载一个ELF程序看似一个EASY的动作,其实下面隐藏了很多很多OS内核的关键实现,让我们一起来解密其中的流程
作者是一个micro kernel的开发者,在设计动态链接器的时候,在此留下一些笔记,重点参考了以下资料文献
- 《程序员的自我修养》
- 《深入理解计算机系统》
- 《现代操作系统-原理与实现》
- 《深入理解LINUX内核》
- 《设计模式/JAVA》
LINUX下的ELF加载器究竟是如何完成的
ELF加载器其实和OS的实现是紧密捆绑在一起的,就像glibc捆绑了linux一样,可移植性是很差的
- LINUX下的ELF加载流程如下
当ELF程序需要解释器(动态链接器)的时候,LINUX内核只完成了初步的解析工作,剩下的工作就转给链接器去完成了,也就是大名鼎鼎的ld.so
Linux下一个最小程序,也必须包括ld.so和libc.so这两个动态库
- ld.so看起来是个动态库,实际上他是静态的,这里说的静态指的是他的运行不依赖任何外部库,只是被编译成了PIC了,算作披着羊皮的狼吧。但是既然ld被编译成了动态库的形式,那么必然有重定位工作要做,也就是对自己的重定位,重定位后就可以正常使用ld.so内的函数和全局变量了
- 有同学说既然是这样,为什么不直接编译静态库算了,链接器自举操作纯属脱了裤子放屁,归根结底还是为了节省内存罢了
glibc下的ld.so链接器流程如下 链接器最重要的工作就是映射依赖库以及重定位工作,映射工作主要依赖mmap这个系统调用,重定位是链接器最复杂的,里面的细节可以参考ELF加载器的原理与实现
- 完成重定位工作后,最后一步将程序转移到crt,crt是一个C运行时环境,CRT有什么用呢?
- 很多同学有疑问?既然链接器都完成了全部工作,不应该跳转到main去运行程序吗?实际上在执行main前我们还需要为main做一点准备工作,这个工作就是由CRT完成
CRT主要有两个工作
- 用户堆管理的初始化
- malloc用户进程的堆分配是放在libc里来完成的,但是这个内存管理器的初始化工作是在crt中完成的!
- 输入输出设备初始化
- 如果要使用printf,实际上最后调用的是write系统调用(fd=STDOUT=1),初始化stdin/stdout/stderr这三个全局变量和相关权限的工作是在crt中完成的
伪代码如下
extern int main(int argc, char *argv[]);
extern int crt_io_initialize(void);
extern int crt_heap_initialize(void);
void exit(int code)
{
}
int crt_main_entry(int argc, char **argv)
{
int ret;
crt_io_initialize();
crt_heap_initialize();
ret = main(argc, argv);
return ret;
}
简单看一下IO是如何初始化的
struct streamlist tg_streamlist;
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
#define stdin (&tg_streamlist.sl_std[STDIN_FILENO])
#define stdout (&tg_streamlist.sl_std[STDOUT_FILENO])
#define stderr (&tg_streamlist.sl_std[STDERR_FILENO])
tg_streamlist.sl_std[0].fs_fd = STDIN_FILENO;
tg_streamlist.sl_std[0].fs_oflags = O_RDONLY;
tg_streamlist.sl_std[1].fs_fd = STDOUT_FILENO;
tg_streamlist.sl_std[1].fs_oflags = O_WROK | O_CREAT;
tg_streamlist.sl_std[2].fs_fd = STDERR_FILENO;
tg_streamlist.sl_std[2].fs_oflags = O_WROK | O_CREAT;
简单看一下用户堆是如何初始化的
先使用brk(0)确定heap的起始地址,然后默认分配132KB内存,再初始化具体的内存管理算法,glibc是ptmalloc,后面的malloc和free就由内存管理算法去分配与释放
int crt_heap_initialize(void)
{
void *base = NULL;
unsigned long heap_size = 132 * 1024;
base = (void *)brk(0);
if (!base) {
return -1;
}
void *end = (void *)((unsigned long)base + heap_size);
end = (void *)brk(end);
if (!end) {
return -1;
}
return mm_heap_initialize(base, heap_size);
}
看一下我们的ELF链接脚本该如何写
ENTRY(crt_main_entry);
SECTIONS
{
. = 0x08040000;
.text :
{
*(.text .text.*)
}
.rodata :
{
*(.rodata .rodata.*)
}
. = ALIGN(CONSTANT (MAXPAGESIZE)) + (. & (CONSTANT (MAXPAGESIZE) - 1));
.data :
{
*(.data .data.*)
}
.bss :
{
*(.bss .bss.*)
}
}
总结一下crt的流程
最后crt最后以crt.o的方式被链接到elf程序中
|