Ubuntu 20.04 glibc 2.31 RTFS + GDB
glibc 在 main 函数之前进行一些设置,以使其某些功能正常工作。让我们尝试追踪此源代码。
hello.c
#include <stdio.h>
int main() {
puts("hello");
return 0;
}
编译和调试:
gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o hello.out hello.c
gdb hello.out
现在在 GDB 中:
b main
r
bt -past-main
给予:
#0 main () at hello.c:3
#1 0x00007ffff7dc60b3 in __libc_start_main (main=0x555555555149 <main()>, argc=1, argv=0x7fffffffbfb8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffbfa8) at ../csu/libc-start.c:308
#2 0x000055555555508e in _start ()
这已经包含了main调用者的代码行:https://github.com/cirosantilli/glibc/blob/glibc-2.31/csu/libc-start.c#L308。
由于glibc的遗留性和通用性水平,该函数有数十亿个ifdef语句,但是对我们产生影响的一些关键部分应该简化为:
# define LIBC_START_MAIN __libc_start_main
STATIC int
LIBC_START_MAIN (int (*main) (int, char **, char **),
int argc, char **argv,
{
result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
exit (result);
}
在调用
__libc_start_main
之前,我们已经处于
_start
程序入口点。通过添加
gcc -Wl,--verbose
我们可以知道这是程序的入口点,因为链接脚本包含以下内容:
ENTRY(_start)
因此,它是动态加载器完成后实际执行的第一个指令。为了在GDB中确认这一点,我们可以使用
-static
进行编译来摆脱动态加载器。
gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o hello.out hello.c
gdb hello.out
然后使 GDB 在执行第一个
starti
指令时停止,并打印第一个指令:
starti
display/12i $pc
这个短语的翻译是:“这给出了:”。
=> 0x401c10 <_start>: endbr64
0x401c14 <_start+4>: xor %ebp,%ebp
0x401c16 <_start+6>: mov %rdx,%r9
0x401c19 <_start+9>: pop %rsi
0x401c1a <_start+10>: mov %rsp,%rdx
0x401c1d <_start+13>: and $0xfffffffffffffff0,%rsp
0x401c21 <_start+17>: push %rax
0x401c22 <_start+18>: push %rsp
0x401c23 <_start+19>: mov $0x402dd0,%r8
0x401c2a <_start+26>: mov $0x402d30,%rcx
0x401c31 <_start+33>: mov $0x401d35,%rdi
0x401c38 <_start+40>: addr32 callq 0x4020d0 <__libc_start_main>
通过在源代码中查找
_start
并关注 x86_64 次数,我们可以看到这似乎对应于
sysdeps/x86_64/start.S:58
。
ENTRY (_start)
/* Clearing frame pointer is insufficient, use CFI. */
cfi_undefined (rip)
/* Clear the frame pointer. The ABI suggests this be done, to mark
the outermost frame obviously. */
xorl %ebp, %ebp
/* Extract the arguments as encoded on the stack and set up
the arguments for __libc_start_main (int (*main) (int, char **, char **),
int argc, char *argv,
void (*init) (void), void (*fini) (void),
void (*rtld_fini) (void), void *stack_end).
The arguments are passed via registers and on the stack:
main: %rdi
argc: %rsi
argv: %rdx
init: %rcx
fini: %r8
rtld_fini: %r9
stack_end: stack. */
mov %RDX_LP, %R9_LP /* Address of the shared library termination
function. */
#ifdef __ILP32__
mov (%rsp), %esi /* Simulate popping 4-byte argument count. */
add $4, %esp
#else
popq %rsi /* Pop the argument count. */
#endif
/* argv starts just at the current stack top. */
mov %RSP_LP, %RDX_LP
/* Align the stack to a 16 byte boundary to follow the ABI. */
and $~15, %RSP_LP
/* Push garbage because we push 8 more bytes. */
pushq %rax
/* Provide the highest stack address to the user code (for stacks
which grow downwards). */
pushq %rsp
#ifdef PIC
/* Pass address of our own entry points to .fini and .init. */
mov __libc_csu_fini@GOTPCREL(%rip), %R8_LP
mov __libc_csu_init@GOTPCREL(%rip), %RCX_LP
mov main@GOTPCREL(%rip), %RDI_LP
#else
/* Pass address of our own entry points to .fini and .init. */
mov $__libc_csu_fini, %R8_LP
mov $__libc_csu_init, %RCX_LP
mov $main, %RDI_LP
#endif
/* Call the user's main function, and exit with its value.
But let the libc call main. Since __libc_start_main in
libc.so is called very early, lazy binding isn't relevant
here. Use indirect branch via GOT to avoid extra branch
to PLT slot. In case of static executable, ld in binutils
2.26 or above can convert indirect branch into direct
branch. */
call *__libc_start_main@GOTPCREL(%rip)
这段话的翻译如下:
最终调用了 __libc_start_main
,符合预期。
不幸的是,-static
使得来自 main
的 bt
显示的信息不够详细:
#0 main () at hello.c:3
#1 0x0000000000402560 in __libc_start_main ()
#2 0x0000000000401c3e in _start ()
如果我们去掉“-static”并从“starti”开始,则会得到以下结果:
=> 0x7ffff7fd0100 <_start>: mov %rsp,%rdi
0x7ffff7fd0103 <_start+3>: callq 0x7ffff7fd0df0 <_dl_start>
0x7ffff7fd0108 <_dl_start_user>: mov %rax,%r12
0x7ffff7fd010b <_dl_start_user+3>: mov 0x2c4e7(%rip),%eax # 0x7ffff7ffc5f8 <_dl_skip_args>
0x7ffff7fd0111 <_dl_start_user+9>: pop %rdx
通过在源代码中使用 grep 命令搜索
_dl_start_user
,可以看出这似乎来自于
sysdeps/x86_64/dl-machine.h:L147。
/* Initial entry point code for the dynamic linker.
The C function `_dl_start' is the real entry point;
its return value is the user program's entry point. */
#define RTLD_START asm ("\n\
.text\n\
.align 16\n\
.globl _start\n\
.globl _dl_start_user\n\
_start:\n\
movq %rsp, %rdi\n\
call _dl_start\n\
_dl_start_user:\n\
# Save the user entry point address in %r12.\n\
movq %rax, %r12\n\
# See if we were run as a command with the executable file\n\
# name as an extra leading argument.\n\
movl _dl_skip_args(%rip), %eax\n\
# Pop the original argument count.\n\
popq %rdx\n\
这可能是动态加载器的入口点。
如果我们在
_start
处断点并继续执行,这似乎会与使用
-static
时结束于相同位置,然后调用
__libc_start_main
。
当我尝试一个 C++ 程序时:
hello.cpp
#include <iostream>
int main() {
std::cout << "hello" << std::endl;
}
使用:
g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o hello.out hello.cpp
结果基本相同,例如在
main
处的回溯完全相同。
我认为C++编译器只是调用钩子来实现任何C++特定的功能,并且在C/C++之间很好地分离了事物。
待办事项:
int user_main()
是一个被调用来初始化int main_ret
的函数,而不是一个构造函数,它会被调用来初始化一个(用户定义的)类。但这也没关系。不仅构造函数在 main 函数之前运行,各种初始化代码也可以在 main 函数之前运行,如 https://en.cppreference.com/w/cpp/language/initialization 中所述,在一个翻译单元内有序地进行非局部动态初始化 3)。 - Don Slowik