了解到大家对于程序如何启动抱有极大的兴趣,尽管我们了解elf的组成原理,但是和程序的启动还是不太一致的,本文通过一个最简单的程序,来浅析一下一个程序的启动过程。便于大家更加深入的了解linux中一个程序的启动过程
我们在聊到C程序的时候,我相信所有人都或多或少了解过谭浩强的C语言书籍,我以第一节为例,如下:
其中的代码内容如下:
#include <stdio.h> int main() { printf("This is a C program.\n"); return 0; }
我们编译后运行如下:
# gcc first_c.c -g -o c && ./c This is a C program.
根据上面的代码,我们获取了第一个c程序,我们./c就能运行。但是这里还远远不够,我们压根还不知道这个程序是如何被运行的。所以我们需要开始调试它
# gdb ./c Reading symbols from ./c... (gdb)
我们先断点在main上然后运行
(gdb) b main Breakpoint 1 at 0x40059c: file first_c.c, line 5. (gdb) r Starting program: /root/c/c [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1". Breakpoint 1, main () at first_c.c:5 5 printf("This is a C program.\n");
此时我们查看一下x30寄存器,这是arm的lr寄存器,保存函数返回地址的
(gdb) x $x30 0x7ff7e3bd90 <__libc_start_main+232>: 0x940055c8
这里gdb友善的提供了提示__libc_start_main+232,我们可以发现是在__libc_start_main的sp指针的+232的位置,根据这个信息,我们拿到了main的上一个调用函数__libc_start_main
此时我们将断点放在__libc_start_main上,重新运行,如下:
(gdb) b __libc_start_main Breakpoint 2 at 0x7ff7e3bca8: file ../csu/libc-start.c, line 141. (gdb) d 1 (gdb) r The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /root/c/c [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1". Breakpoint 2, __libc_start_main (main=0x4004d8 <__wrap_main>, argc=1, argv=0x7ffffff1d8, init=0x4005b8 <__libc_csu_init>, fini=0x400638 <__libc_csu_fini>, rtld_fini=0x7ff7fdac90, stack_end=0x7ffffff1d0) at ../csu/libc-start.c:141 141 ../csu/libc-start.c: 没有那个文件或目录.
然后我们继续查看x30的值
(gdb) x $x30 0x4004d4 <_start+52>: 0x97ffffeb
这里可以发现是_start函数调用了__libc_start_main
我们继续跟踪,如下:
(gdb) b _start Breakpoint 3 at 0x4004ac (gdb) d 2 (gdb) r The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /root/c/c [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1". Breakpoint 3, 0x00000000004004ac in _start ()
此时我们查看x30,发现了一个现象如下:
(gdb) x $x30 0x0: Cannot access memory at address 0x0
这里可以发现,_start没有满足调用约定了。
对于一个函数,如果不遵守调用约定了,那么我们可以知道,这个函数一定是第一个执行的函数,没有比它更早的函数调用。
我们可以通过readelf能够看到,如下:
# readelf -h c ELF 头: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 类别: ELF64 数据: 2 补码,小端序 (little endian) Version: 1 (current) OS/ABI: UNIX - System V ABI 版本: 0 类型: EXEC (可执行文件) 系统架构: AArch64 版本: 0x1 入口点地址: 0x4004ac 程序头起点: 64 (bytes into file) Start of section headers: 7256 (bytes into file) 标志: 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 这里我们看到入口点地址是:0x4004ac
可以发现,我们的elf文件c程序,默认的ld的入口地址是0x4004ac,这个地址就是_start函数。这个_start函数最终会调用到main上。
根据上面我们知道,程序第一个函数入口是_start,我们可以对这个函数反汇编,如下:
(gdb) disassemble Dump of assembler code for function _start: 0x00000000004004a0 <+0>: mov x29, #0x0 // #0 0x00000000004004a4 <+4>: mov x30, #0x0 // #0 0x00000000004004a8 <+8>: mov x5, x0 => 0x00000000004004ac <+12>: ldr x1, [sp] 0x00000000004004b0 <+16>: add x2, sp, #0x8 0x00000000004004b4 <+20>: mov x6, sp 0x00000000004004b8 <+24>: adrp x0, 0x400000 0x00000000004004bc <+28>: add x0, x0, #0x4d8 0x00000000004004c0 <+32>: adrp x3, 0x400000 0x00000000004004c4 <+36>: add x3, x3, #0x5b8 0x00000000004004c8 <+40>: adrp x4, 0x400000 0x00000000004004cc <+44>: add x4, x4, #0x638 0x00000000004004d0 <+48>: bl 0x400460 <__libc_start_main@plt> 0x00000000004004d4 <+52>: bl 0x400480 <abort@plt> End of assembler dump.
可以发现,这里默认设置x29和x30为0,然后跳转到0x400460 <__libc_start_main@plt>
上,我们随机跟到代码中,如下:
这里以glibc-2.31为例,glibc-2.31/sysdeps/aarch64/start.S
这里_start函数如下
.text .globl _start .type _start,#function _start: /* Create an initial frame with 0 LR and FP */ mov x29, #0 mov x30, #0 /* Setup rtld_fini in argument register */ mov x5, x0 /* Load argc and a pointer to argv */ ldr PTR_REG (1), [sp, #0] add x2, sp, #PTR_SIZE /* Setup stack limit in argument register */ mov x6, sp #ifdef PIC # ifdef SHARED adrp x0, :got:main ldr PTR_REG (0), [x0, #:got_lo12:main] adrp x3, :got:__libc_csu_init ldr PTR_REG (3), [x3, #:got_lo12:__libc_csu_init] adrp x4, :got:__libc_csu_fini ldr PTR_REG (4), [x4, #:got_lo12:__libc_csu_fini] # else adrp x0, __wrap_main add x0, x0, :lo12:__wrap_main adrp x3, __libc_csu_init add x3, x3, :lo12:__libc_csu_init adrp x4, __libc_csu_fini add x4, x4, :lo12:__libc_csu_fini # endif #else /* Set up the other arguments in registers */ MOVL (0, main) MOVL (3, __libc_csu_init) MOVL (4, __libc_csu_fini) #endif /* __libc_start_main (main, argc, argv, init, fini, rtld_fini, stack_end) */ /* Let the libc call main and exit with its return code. */ bl __libc_start_main /* should never get here....*/ bl abort #if defined PIC && !defined SHARED /* When main is not defined in the executable but in a shared library then a wrapper is needed in crt1.o of the static-pie enabled libc, because crt1.o and rcrt1.o share code and the later must avoid the use of GOT relocations before __libc_start_main is called. */ __wrap_main: b main #endif /* Define a symbol for the first piece of initialized data. */ .data .globl __data_start __data_start: .long 0 .weak data_start data_start = __data_start
这里很明显做了如下:
我们可以翻阅glibc的代码,在csu/libc-start.c
有函数的定义,如下:
#ifdef LIBC_START_MAIN # ifdef LIBC_START_DISABLE_INLINE # define STATIC static # else # define STATIC static inline __attribute__ ((always_inline)) # endif #else # define STATIC # define LIBC_START_MAIN __libc_start_main #endif STATIC int LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL), int argc, char **argv, #ifdef LIBC_START_MAIN_AUXVEC_ARG ElfW(auxv_t) *auxvec, #endif __typeof (main) init, void (*fini) (void), void (*rtld_fini) (void), void *stack_end) { ........ exit (result); }
这里我们留意参数可以知道需要调用如下:
__typeof (main) init void (*fini) (void) void (*rtld_fini) (void), void *stack_end) exit (result);
这里可以知道,__libc_start_main会调用.init,.fini ,mian和exit以及rtld_fini。
这里.init 和.fini 是用作代码运行时构造和析构的默认回调接口
这里rtld_fini 是ld的清理函数。接下来注意解析:
此函数是x5寄存器
(gdb) x $x5 0x7ff7fdac90: 0xa9b87bfd
我们可以gdb内部设置断点如下:
(gdb) b *0x7ff7fdac90 Breakpoint 4 at 0x7ff7fdac90
然后继续运行
(gdb) c Continuing. This is a C program. Breakpoint 4, 0x0000007ff7fdac90 in ?? () from /lib/ld-linux-aarch64.so.1
可以发现程序正常运行推出后,会断点停在/lib/ld-linux-aarch64.so.1
内部。
但是我们进行反汇编却看不到代码调用,说明实际上是一个栈区地址而已,并不是完整的函数
(gdb) disassemble No function contains program counter for selected frame.
我们知道.init函数是x3,所以可以根据如下汇编计算
0x00000000004004c0 <+32>: adrp x3, 0x400000 0x00000000004004c4 <+36>: add x3, x3, #0x5b8
我们加上断点后运行如下:
(gdb) b *0x4005b8 Breakpoint 5 at 0x4005b8 (gdb) c Continuing. Breakpoint 5, 0x00000000004005b8 in __libc_csu_init ()
可以发现函数是__libc_csu_init
,我们反汇编
(gdb) disassemble Dump of assembler code for function __libc_csu_init: => 0x00000000004005b8 <+0>: stp x29, x30, [sp, #-64]! 0x00000000004005bc <+4>: mov x29, sp 0x00000000004005c0 <+8>: stp x19, x20, [sp, #16] 0x00000000004005c4 <+12>: adrp x20, 0x410000 0x00000000004005c8 <+16>: add x20, x20, #0xdf0 0x00000000004005cc <+20>: stp x21, x22, [sp, #32] 0x00000000004005d0 <+24>: adrp x21, 0x410000 0x00000000004005d4 <+28>: add x21, x21, #0xde8 0x00000000004005d8 <+32>: sub x20, x20, x21 0x00000000004005dc <+36>: mov w22, w0 0x00000000004005e0 <+40>: stp x23, x24, [sp, #48] 0x00000000004005e4 <+44>: mov x23, x1 0x00000000004005e8 <+48>: mov x24, x2 0x00000000004005ec <+52>: bl 0x400420 <_init> 0x00000000004005f0 <+56>: cmp xzr, x20, asr #3 0x00000000004005f4 <+60>: b.eq 0x400620 <__libc_csu_init+104> // b.none 0x00000000004005f8 <+64>: asr x20, x20, #3 0x00000000004005fc <+68>: mov x19, #0x0 // #0 0x0000000000400600 <+72>: ldr x3, [x21, x19, lsl #3] 0x0000000000400604 <+76>: mov x2, x24 0x0000000000400608 <+80>: add x19, x19, #0x1 0x000000000040060c <+84>: mov x1, x23 0x0000000000400610 <+88>: mov w0, w22 0x0000000000400614 <+92>: blr x3 0x0000000000400618 <+96>: cmp x20, x19 0x000000000040061c <+100>: b.ne 0x400600 <__libc_csu_init+72> // b.any 0x0000000000400620 <+104>: ldp x19, x20, [sp, #16] 0x0000000000400624 <+108>: ldp x21, x22, [sp, #32] 0x0000000000400628 <+112>: ldp x23, x24, [sp, #48] 0x000000000040062c <+116>: ldp x29, x30, [sp], #64 0x0000000000400630 <+120>: ret End of assembler dump.
这里可以看到会跳转到_init函数,我们继续断点
(gdb) b *_init Breakpoint 6 at 0x7ff7e3bc08: file init-first.c, line 55.
运行如下:
(gdb) r Starting program: /root/c/c [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1". Breakpoint 6, _init (argc=1, argv=0x7ffffff1d8, envp=0x7ffffff1e8) at init-first.c:55 55 init-first.c: 没有那个文件或目录.
此时进入_init函数内部
(gdb) c Continuing. Breakpoint 18, 0x0000000000400420 in _init () (gdb) disassemble Dump of assembler code for function _init: => 0x0000000000400420 <+0>: stp x29, x30, [sp, #-16]! 0x0000000000400424 <+4>: mov x29, sp 0x0000000000400428 <+8>: bl 0x4004dc <call_weak_fn> 0x000000000040042c <+12>: ldp x29, x30, [sp], #16 0x0000000000400430 <+16>: ret End of assembler dump.
可以发现,其就是运行的默认的init回调,我们可以objdump查看如下:
objdump -d c.o Disassembly of section .init: 0000000000400420 <_init>: 400420: a9bf7bfd stp x29, x30, [sp, #-16]! 400424: 910003fd mov x29, sp 400428: 9400002d bl 4004dc <call_weak_fn> 40042c: a8c17bfd ldp x29, x30, [sp], #16 400430: d65f03c0 ret
我们知道fini是保存在x4,所以如下:
0x00000000004004c8 <+40>: adrp x4, 0x400000 0x00000000004004cc <+44>: add x4, x4, #0x638
此时我们计算出函数如下:
(gdb) x 0x400638 0x400638 <__libc_csu_fini>: 0xd65f03c0
我们直接反汇编如下:
(gdb) disassemble __libc_csu_fini Dump of assembler code for function __libc_csu_fini: 0x0000000000400638 <+0>: ret End of assembler dump.
可以看到此函数直接是一个返回
我们通过objdump如下:
Disassembly of section .fini: 000000000040063c <_fini>: 40063c: a9bf7bfd stp x29, x30, [sp, #-16]! 400640: 910003fd mov x29, sp 400644: a8c17bfd ldp x29, x30, [sp], #16 400648: d65f03c0 ret
可以发现一致。
我们知道main函数是__libc_start_main中直接调用的我们反汇编后,注意这个语句
0x0000007ff7e3bd88 <+224>: ldr x3, [sp, #104] 0x0000007ff7e3bd8c <+228>: blr x3
代码运行如下:
(gdb) b *0x0000007ff7e3bd8c Breakpoint 31 at 0x7ff7e3bd8c: file ../csu/libc-start.c, line 308. (gdb) c Continuing. Breakpoint 31, 0x0000007ff7e3bd8c in __libc_start_main (main=0x4004d8 <__wrap_main>, argc=1, argv=0x7ffffff1d8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=<optimized out>) at ../csu/libc-start.c:308 308 in ../csu/libc-start.c
可以发现其跳转了x3,然后以0x0000007ff7e3bd90作为lr,我们计算sp+104的值,如下:
(gdb) x $sp+104 0x7ffffff0e8: 0x004004d8
可以发现blr会跳转到0x004004d8,我们解释这个地址
(gdb) x 0x004004d8 0x4004d8 <__wrap_main>: 0x1400002f
然后打上断点
(gdb) b *0x004004d8 Breakpoint 1 at 0x4004d8 (gdb) r Breakpoint 1, 0x00000000004004d8 in __wrap_main ()
然后反汇编
(gdb) disassemble Dump of assembler code for function __wrap_main: => 0x00000000004004d8 <+0>: b 0x400594 <main> End of assembler dump.
这里会直接跳转到mian,我们继续运行
(gdb) r Breakpoint 3, 0x0000000000400594 in main ()
然后再反汇编main函数,如下
(gdb) disassemble Dump of assembler code for function main: => 0x0000000000400594 <+0>: stp x29, x30, [sp, #-16]! 0x0000000000400598 <+4>: mov x29, sp 0x000000000040059c <+8>: adrp x0, 0x400000 0x00000000004005a0 <+12>: add x0, x0, #0x668 0x00000000004005a4 <+16>: bl 0x400490 <puts@plt> 0x00000000004005a8 <+20>: mov w0, #0x0 // #0 0x00000000004005ac <+24>: ldp x29, x30, [sp], #16 0x00000000004005b0 <+28>: ret End of assembler dump.
然后我们将c程序直接objdump,如下
0000000000400594 <main>: 400594: a9bf7bfd stp x29, x30, [sp, #-16]! 400598: 910003fd mov x29, sp 40059c: 90000000 adrp x0, 400000 <_init-0x420> 4005a0: 9119a000 add x0, x0, #0x668 4005a4: 97ffffbb bl 400490 <puts@plt> 4005a8: 52800000 mov w0, #0x0 // #0 4005ac: a8c17bfd ldp x29, x30, [sp], #16 4005b0: d65f03c0 ret 4005b4: d503201f nop
可以发现是完全一样的。这里我们正常跟踪到了main函数
通过此程序,我们可以知道一个程序的启动过程,从elf作为开始,ld加载了entrypoint
后,是通过启动了_start
的text label后,然后再逐渐调用到main函数的。
相当于从程序的最开头,进行了简单的了解了一个程序的启动过程。