-
-
helloworld减肥 + 单步执行原理
-
发表于: 2024-5-23 20:13 3192
-
最近为了了解一下ORC和Dwarf,搜到了这篇文章:
https://eli.thegreenplace.net/2011/02/07/how-debuggers-work-part-3-debugging-information/
然后被作者带偏:
How debuggers work: Part 1 - Basics
How debuggers work: Part 2 - Breakpoints
How debuggers work: Part 3 - Debugging information
后面我会陆续(工作第一),按照自己的理解和补充,整理一份笔记。
本篇仅为Part 1的部分内容 -- helloworld减肥,以及基于作者提供的简易调度程序,我补充了对单步执行原理的分析。
作者先花一些篇幅介绍helloworld减肥,我猜测有两个原因:
一方面,我们通常编译出来的helloworld程序,执行的指令会有10万条左右(我的环境89507条),这可能违背了大多数人的直觉,如果不事先阐明,读者后续可能会误认为,是他的调试程序输出有问题;
第二方面,将helloworld执行指令缩减到7条,可以使后续实验结果更简洁。
1 2 3 4 5 6 7 | #include <stdio.h> int main() { printf ( "Hello, world!\n" ); return 0; } |
编译:
1 2 | gcc helloworld.c -o helloworld -g -Wall # 动态链接libc.so.6 gcc helloworld.c -o helloworld_static - static -g -Wall # 安装libc.a: yum install -y glibc- static .x86_64 |
Part 1提供的调试器代码,用于统计被调试程序运行时,会执行多少条指令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/ptrace.h> #include <sys/user.h> #define procmsg printf void run_target( const char * programname) { procmsg( "target started. will run '%s'\n" , programname); /* Allow tracing of this process */ if (ptrace(PTRACE_TRACEME, 0, 0, 0) < 0) { perror ( "ptrace" ); return ; } /* Replace this process's image with the given program */ execl(programname, programname, ( char *)NULL); } void run_debugger(pid_t child_pid) { int wait_status; unsigned icounter = 0; procmsg( "debugger started\n" ); /* Wait for child to stop on its first instruction */ wait(&wait_status); while (WIFSTOPPED(wait_status)) { icounter++; /* Make the child execute another instruction */ if (ptrace(PTRACE_SINGLESTEP, child_pid, 0, 0) < 0) { perror ( "ptrace" ); return ; } /* Wait for child to stop on its next instruction */ wait(&wait_status); } procmsg( "the child executed %u instructions\n" , icounter); } int main( int argc, char ** argv) { pid_t child_pid; if (argc < 2) { fprintf (stderr, "Expected a program name as argument\n" ); return -1; } child_pid = fork(); if (child_pid == 0) run_target(argv[1]); // 子进程 else if (child_pid > 0) run_debugger(child_pid); // 父进程 else { perror ( "fork" ); return -1; } return 0; } |
编译:
1 | gcc simple_tracer.c -o simple_tracer -g -Wall |
指令统计:
1 2 | simple_tracer helloworld # 不同环境会有差别,我的环境:89507 simple_tracer helloworld_static # 不同环境会有差别,我的环境:9593 |
helloworld执行指令包括:_dl_runtime_resolve() + __libc_start_main() + helloworld.main()。其中,_dl_runtime_resolve()用于处理重定项和加载libc.so.6。
helloworld_static执行指令包括:__libc_start_main() + helloworld.main()。__libc_start_main()函数,直接静态链接到了helloworld_static可执行文件里,所以运行时,少了加载libc.so.6的过程。
libc实际上就是对系统调用的封装(可执行文件->libc库函数->系统调用),helloworld程序,完全可以自己直接调用系统调用:
查看内核代码(arch/i386/kernel/entry.S),核实系统调用号:
由于Part 1提供的汇编代码是32位的,而我的环境为64位系统,所以nasm加了"-f elf32"选项,gcc加了"-m32"选项:
1 2 3 4 5 6 | # 左边为intel格式,用nasm编译 nasm helloworld.s -f elf32 -o helloworld.o gcc helloworld.o -nostdlib -o helloworld -m32 # 右边为AT&T格式,如果不方便安装nasm,就可以选择这种 gcc helloworld.s -o helloworld -nostdlib -m32 |
-nostdlib表示不链接libc库,再次统计helloworld执行的指令:
1 | simple_tracer helloworld # 仅7条指令 |
保护模式下,每个进程有自己独立的虚拟空间,相互隔离,simple_tracer是怎么让helloworld,每执行一条指令都停下来,并通知自己的?
这要依赖内核和硬件的支持:
内核:sys_ptrace()、do_signal()函数
硬件:EFLAGS寄存器.TF标志位置1后,每执行完一条指令,就会触发异常
理解了单步执行原理,其它基本就是小咔啦咪了:
查看变量:本质上就是读helloworld进程空间的内存。
断点:本质上就是写helloworld进程空间的内存,将断点处指令第一个字节,改为0xCC("int 3"指令),至于gdb如何根据函数名、X.c+行号,计算出汇编指令的加载地址,这是Part 3的内容,后续有空的话再整理,或者可以看看我另外一篇笔记:gcc -O2编译,gdb单步执行怪怪的,虽然只涉及了一点点.debug节区的利用,但其实已经可以建立对调试信息最重要的认识。

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
赞赏
- CAS (compare and set) 1328
- shell+Makefile,坑我晚下班 2512
- 调试器:显示调用栈 2965
- helloworld减肥 + 单步执行原理 3193
- gcc -O2编译,gdb单步执行怪怪的 10828