首页
社区
课程
招聘
helloworld减肥 + 单步执行原理
发表于: 2024-5-23 20:13 2955

helloworld减肥 + 单步执行原理

2024-5-23 20:13
2955

最近为了了解一下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节区的利用,但其实已经可以建立对调试信息最重要的认识。


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2024-5-23 20:57 被kanxue编辑 ,原因:
上传的附件:
收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//