-
-
helloworld减肥 + 单步执行原理
-
发表于: 2024-5-23 20:13 2990
-
最近为了了解一下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条,可以使后续实验结果更简洁。
#include <stdio.h> int main() { printf("Hello, world!\n"); return 0; }
编译:
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提供的调试器代码,用于统计被调试程序运行时,会执行多少条指令:
#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; }
编译:
gcc simple_tracer.c -o simple_tracer -g -Wall
指令统计:
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"选项:
# 左边为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执行的指令:
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节区的利用,但其实已经可以建立对调试信息最重要的认识。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
赞赏
- shell+Makefile,坑我晚下班 2150
- 调试器:显示调用栈 2678
- helloworld减肥 + 单步执行原理 2991
- gcc -O2编译,gdb单步执行怪怪的 10422
- systemtap追踪自己开发的内核模块 11809