根据shell/exec加载elf程序的原理,是先解析ELF,
定位并加载ld-linux.so这个程序加载器(该so路径需要通过ELF直接确定,且不依赖其他库),
然后再解析elf中需要load的共享库,但这之前可以通过LD_PRELOAD来提前预先加载共享库
- next step
https://zhuanlan.zhihu.com/p/188276588
-------------------------
hello 源代码为
int main(int argc, char const *argv[])
{
return 0;
}
编译过程为静态编译
hello程序怎么被加载到内存
1.加载了哪些东西到内存
2.加载到哪些位置
3.什么命令A加载的
4.命令A是如何被加载的
hello程序是怎么开始运行的
1.hello程序的内存分布
2.怎么转交给hello运行的
3.从什么地址开始运行
4.hello程序返回之后做了什么
-------------------------
hello.c 不变,更改为动态编译
hello.c 添加printf , 更改为动态编译
- i686 外部程序 执行过程
ELF loader 分为 内核空间Loader(Program loader) 和 用户空间Linker(Program interpreter,ld-xxx.so,/lib/ld-linux.so.2指向的文件)
Program loader 内核提供代码,负责加载segement
Program interpreter C库(glibc)提供代码,负责重定位和链接动态库
注意 ELF image 与 Process Image 的不同
ELF image : 程序二进制文件,对应实体文件
Process Image : 进程在3G虚拟空间的布局,及各个段加载过后的状态
1.用户空间(用户在shell中执行外部程序,通过系统调用进入内核)
用户在shell执行外部程序
shell以 fork+exec syscall 的方式执行外部程序
通过 int 0x80 软件终端触发 kernel 的 exec syscall 服务 (sys_execvp)
2.内核空间(执行Program loader,载入外部程序二进制文件)
exec syscall 执行 内核中的ELF Loader(Program loader) ,载入与建立 外部程序的 ELF image
3. 整个 Process Image 的建立(内核空间(Loader)和用户空间(Linker))
3.1 内核空间(Program loader 装入 外部程序的text和data和bss,装入program interpreter,并exec program interpreter)
Program loader 找到 PT_INTERP segment
Program loader 将 PT_LOAD segment mapping 为新的 text/data segment
text segment 由虚拟地址 0x0804 8000 开始 ,data segment 紧跟其后
Program loader 将 BSS segment 准备好
Program loader 呼叫 interpreter loader 将 program interpreter(ld.so)载入,并mapping到process memory
interpreter 的 text segment 由 虚拟地址 0x4000 0000 开始, interpreter 的data segment 紧跟其后
Program loader 将 process 的 register %eip(user-mode)修改为 program interpreter 的 进入点,并将 %eip设定为usermode 的stack , 从而 program interpreter 开始执行
3.2 用户空间(program interpreter 加载 外部程序所需的 动态库)
Program interpreter 会找到 process 所需的 shared library(名称与路径)
program interpreter 通过mamp ,将 shared library 给 mapping 到 process memory , 以完成整个 Process Image 的建立
program interpreter 更新共享库的符号表(symbol table)
4.用户空间(program interpreter 调用 外部程序的main函数)
program interpreter 执行 x86 jump 动作,到 process 的 entry point (entry point被记录在ELF header中)
程序从entry point(一般为main函数)开始执行
参考文档
-
深入浅出HelloWorld (HackingHelloWorld)_PartIII.pdf
-
1 加载应用程序过程中 内核空间的工作
内核的工作
内核首先读取ELF文件头部,再读如各种数据结构,从这些数据结构中可知各段或节的地址及标识,然后调用mmap()把找到的可加载段的内容加载到内存中。同时读取段标记,以标识该段在内存中是否可读、可写、可执行。其中,文本段是程序代码,只读且可执行,而数据段是可读且可写。
从PT_INTERP的段中找到所对应的动态链接器名称,并加载动态链接器。通常是/lib/ld-linux.so.2.
内核把新进程的堆栈中设置一些标记对,以指示动态链接器的相关操作。
内核把控制权传递给动态链接器。
动态链接器的工作并不是在内核空间完成的, 而是在用户空间完成的, 比如C语言程序则交给C运行时库来完成
fs/exec.c
SYSCALL_DEFINE3(execve,const char __user *, filename,const char __user *const __user *, argv,const char __user *const __user *, envp)
do_execve
do_execveat_common
bprm_execve
exec_binprm
search_binary_handler
fmt->load_binary(bprm); // 对应elf为load_elf_binary
load_elf_binary
...
create_elf_tables
start_thread(regs, elf_entry, bprm->p);
// start_thread()这个宏操作会将eip和esp改成新的地址,就使得CPU在返回用户空间时就进入新的程序入口。
// 如果存在解释器映像,那么这就是解释器映像的程序入口,否则就是目标映像的程序入口。
- 2 加载应用程序过程中 用户空间的工作
动态链接器的工作
动态链接器检查程序对共享库的依赖性,并在需要时对其进行加载。
动态链接器对程序的外部引用进行重定位,并告诉程序其引用的外部变量/函数的地址,此地址位于共享库被加载在内存的区间内。动态链接还有一个延迟定位的特性,即只有在“真正”需要引用符号时才重定位,这对提高程序运行效率有极大帮助。
动态链接器执行在ELF文件中标记为.init的节的代码,进行程序运行的初始化。
动态链接器把控制传递给程序,从ELF文件头部中定义的程序进入点(main)开始执行。在a.out格式和ELF格式中,程序进入点的值是显式存在的,而在COFF格式中则是由规范隐含定义。
程序开始执行
动态连接器其实 就是 glibc编译出来的 ld.so
ld.so入口为
elf/rtld.c 中的 RTLD_START // RTLD_START 在 sysdeps/i386/dl-machine.h 中定义
ld的整个过程(加载库,调用main)都在 RTLD_START 中描述了,其中调用了 _dl_start (为RTLD_START 的主体部分)
_dl_start 返回了 user program's entry point
RTLD_START
_dl_start
_dl_start_final
...
_dl_sysdep_start
dl_main
process_envars
return ELF_MACHINE_START_ADDRESS (GL(dl_ns)[LM_ID_BASE]._ns_loaded, entry);
...
# Jump to the user's entry point.\n\
jmp *%edi\n\ // 这一句就是跳转到 应用程序的入口(一般是main函数)
.previous\n\