目录
看到一篇好文:摘抄记录--Linux应用程序 启动流程-BugMan-ChinaUnix博客
-
代码
#include <stdio.h>
int main (int argc, char *argv[])
{
printf ("Hello World\n");
return 0;
}
-
用strace查看调用流程
pc123@ubuntu:~/Public$ strace ./main execve("./main", ["./main"], 0x7fff3906f3d0 /* 56 vars */) = 0 brk(NULL) = 0x55d0fc755000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=81045, ...}) = 0 mmap(NULL, 81045, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fe65b878000 close(3) = 0 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\240\35\2\0\0\0\0\0"..., 832) = 832 fstat(3, {st_mode=S_IFREG|0755, st_size=2030928, ...}) = 0 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fe65b876000 mmap(NULL, 4131552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fe65b272000 mprotect(0x7fe65b459000, 2097152, PROT_NONE) = 0 mmap(0x7fe65b659000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7fe65b659000 mmap(0x7fe65b65f000, 15072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fe65b65f000 close(3) = 0 arch_prctl(ARCH_SET_FS, 0x7fe65b8774c0) = 0 mprotect(0x7fe65b659000, 16384, PROT_READ) = 0 mprotect(0x55d0fa83f000, 4096, PROT_READ) = 0 mprotect(0x7fe65b88c000, 4096, PROT_READ) = 0 munmap(0x7fe65b878000, 81045) = 0 fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 14), ...}) = 0 brk(NULL) = 0x55d0fc755000 brk(0x55d0fc776000) = 0x55d0fc776000 write(1, "hello world", 11hello world) = 11 exit_group(0) = ? +++ exited with 0 +++ pc123@ubuntu:~/Public$
-
分析
- 毫无疑问,在shell内执行一个程序main,本质上是shell去调用execve函数执行main程序
- execve是一个系统调用,Linux内核会在这个系统调用里面为main程序映射必要的内存
- 动态解释器【动态解释器】:当execve将控制权给/lib64/ld-linux-x86-64.so.2的时候,这个文件进行动态库代码重定向等功能
$ readelf -l hello | grep interpreter [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
- 通过读取elf头部信息中的entry部分,即可得到程序入口地址为0x540
pc123@ubuntu:~/Public$ readelf -h main 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: DYN (Shared object file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x540 Start of program headers: 64 (bytes into file) Start of section headers: 6448 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 9 Size of section headers: 64 (bytes) Number of section headers: 29 Section header string table index: 28 pc123@ubuntu:~/Public$
- 通过objdump找到程序的入口地址:
pc123@ubuntu:~/Public$ objdump -axd main > main.s
在main.s文件找到地址为0x540的代码
Disassembly of section .text: 段名:.text,表示代码段 0000000000000540 <_start>: 函数名_start,存放地址0x540 540: 31 ed xor %ebp,%ebp 542: 49 89 d1 mov %rdx,%r9 545: 5e pop %rsi 546: 48 89 e2 mov %rsp,%rdx 549: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp 54d: 50 push %rax 54e: 54 push %rsp 54f: 4c 8d 05 8a 01 00 00 lea 0x18a(%rip),%r8 # 6e0 <__libc_csu_fini> 556: 48 8d 0d 13 01 00 00 lea 0x113(%rip),%rcx # 670 <__libc_csu_init> 55d: 48 8d 3d e6 00 00 00 lea 0xe6(%rip),%rdi # 64a <main> 564: ff 15 76 0a 20 00 callq *0x200a76(%rip) # 200fe0 <__libc_start_main@GLIBC_2.2.5> 56a: f4 hlt 56b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
可见在shell执行main程序,程序的入口是_start函数,这个_start函数实际上是由C库实现的,
_start函数最后调用了__libc_start_main函数【汇编的callq *0x200a76这一行】,而调用__libc_start_main函数传入了0x54f, 0x556, 0x55d对应的这三个参数,其中对应的就是__libc_csu_fini, __libc_csu_initial, main函数【也就是我们写的代码】
【__libc_start_main@plt表示这是一个延迟加载函数,什么是延迟加载函数呢?延迟加载函数就是指在动态解释阶段不进行代码重定位,只有在真正使用该函数的时候,才去定位该函数的地址, 这样做的目的是加快程序启动】
__libc_start_main函数的运行的顺序为:__libc_csu_init->main->__libc_csu_fini,__libc_csu_init叫构造函数,__libc_csu_fini叫析构函数,在代码中可通过__attribute__ ((constructor))标记构造函数,__attribute__ ((destructor))来标记析构函数 有这样的例子
#include <stdio.h> static void hello_after() __attribute__ ((destructor)); static void hello_before() __attribute__ ((constructor)); static void hello_before(void) { printf("Before main\n"); } static void hello_after(void) { printf("After main\n"); } int main (int argc, char *argv[]) { printf ("Hello World\n"); return 0; }
pc123@ubuntu:~/Public$ ./main Before main Hello World After main pc123@ubuntu:~/Public$
-
总结
shell通过execve启动main程序------->main函数被C 库作为一个函数----------->整个程序的入口地址是_start【通过objdump对应的0x540确定】----->start函数调用了__libc_start_main,传入了构造,main, 析构函数。