【问题标题】:Tracing a program from its entry point with ptrace (linux, c)使用 ptrace (linux, c) 从入口点跟踪程序
【发布时间】:2021-02-12 05:31:19
【问题描述】:

我想使用 ptrace 跟踪程序的寄存器和指令。为了更好地理解我的代码,我将其减少到只计算“/bin/ls”的指令数量的程度。

这是我的代码(忽略不必要的包含):

#include <stdio.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <sys/reg.h>    
#include <sys/syscall.h>

int main()
{   
    pid_t child;
    child = fork(); //create child
    
    if(child == 0) {
        ptrace(PTRACE_TRACEME, 0, NULL, NULL);
        char* child_argv[] = {"/bin/ls", NULL};
        execv("/bin/ls", child_argv);
    }
    else {
        int status;
        long long ins_count = 0;
        while(1)
        {
            //stop tracing if child terminated successfully
            wait(&status);
            if(WIFEXITED(status))
                break;

                ins_count++;
                ptrace(PTRACE_SINGLESTEP, child, NULL, NULL);
        }

    printf("\n%lld Instructions executed.\n", ins_count);

    }
    
    return 0;
}

当我运行此代码时,我得到“484252 指令已执行”,我对此表示怀疑。我搜索了一下,发现这些指令大部分来自在执行实际程序(/bin/ls)之前加载库。

如何跳过单步执行到 /bin/ls 的第一条实际指令并从那里开始计数?

【问题讨论】:

    标签: c linux exec system-calls ptrace


    【解决方案1】:

    您是对的,您的计数包括正在执行其工作的动态链接器(并且在二进制文件开始执行之前,AFAIK 是一条幻影指令)。

    (我使用的是 shell 命令,但也可以使用 C 代码完成,使用 elf.h;请参阅 musl dynamic linker 以获得一个很好的示例)

    你可以:

    • 解析/bin/ls的ELF标头以找到入口点和包含入口点的程序标头(我在这里使用cat,因为在我写这篇文章时更容易让它长时间运行)
    # readelf -l /bin/cat
    
    Elf file type is EXEC (Executable file)
    Entry point 0x4025b0
    There are 9 program headers, starting at offset 64
    
    Program Headers:
      Type           Offset             VirtAddr           PhysAddr
                     FileSiz            MemSiz              Flags  Align
    (...)
      LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                     0x000000000000b36c 0x000000000000b36c  R E    0x200000
    (...)
    

    入口点位于 VirtAddr 和 VirtAddr+FileSiz 之间,标志包括可执行位 (E),因此看起来我们走在正确的轨道上。

    注意:Elf file type is EXEC(而不是DYN)意味着我们总是将程序头映射到 VirtAddr 中指定的固定位置;这意味着对于我构建的cat,我们可以只使用我们在上面找到的入口点地址。 DYN 二进制文件可以——并且被——加载到任意地址,因此我们需要进行这种重定位。

    • 找到二进制文件的实际加载地址

    AFAIK 程序头按 VirtAddr 排序,因此带有 LOAD 标志的第一个段将映射到最低地址。打开 /proc/&lt;pid&gt;/maps 并查找您的二进制文件:

    # grep /bin/cat /proc/7431/maps
    00400000-0040c000 r-xp 00000000 08:03 1046541                            /bin/cat
    0060b000-0060c000 r--p 0000b000 08:03 1046541                            /bin/cat
    0060c000-0060d000 rw-p 0000c000 08:03 1046541                            /bin/cat
    

    第一个段映射到 0x00400000(ELF 类型 == EXEC 应为该段)。如果不是,则需要调整入口点地址:

    actual_entrypoint_addr = elf_entrypoint_addr - elf_virt_addr_of_first_phdr + actual_addr_of_first_phdr

    • actual_entrypoint_addr 上设置断点并调用ptrace(PTRACE_CONT)。一旦断点命中(waitpid() 返回),按照目前的方式继续操作(计算ptrace(PTRACE_SINGLESTEP)s)。

    我们需要处理重定位的示例:

    # readelf -l /usr/sbin/nginx
    
    Elf file type is DYN (Shared object file)
    Entry point 0x24e20
    There are 9 program headers, starting at offset 64
    
    Program Headers:
      Type           Offset             VirtAddr           PhysAddr
                     FileSiz            MemSiz              Flags  Align
    (...)
      LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                     0x000000000010df54 0x000000000010df54  R E    0x200000
    (...)
    
    # grep /usr/sbin/nginx /proc/1425/maps
    55e299e78000-55e299f86000 r-xp 00000000 08:03 660029                     /usr/sbin/nginx
    55e29a186000-55e29a188000 r--p 0010e000 08:03 660029                     /usr/sbin/nginx
    55e29a188000-55e29a1a4000 rw-p 00110000 08:03 660029                     /usr/sbin/nginx
    

    入口点位于 0x55e299e78000 - 0 + 0x24e20 == 0x55e299e9ce20

    【讨论】: