Rumah  >  Artikel  >  Tutorial sistem  >  Analisis prinsip kerja penyahpepijat (3): Kajian mendalam tentang maklumat penyahpepijatan

Analisis prinsip kerja penyahpepijat (3): Kajian mendalam tentang maklumat penyahpepijatan

WBOY
WBOYke hadapan
2024-01-01 20:05:221314semak imbas
Pengenalan Artikel ini akan menerangkan cara penyahpepijat kelihatan dalam kod mesin untuk fungsi bahasa C, pembolehubah dan data yang diperlukan untuk menukar kod sumber bahasa C kepada kod bahasa mesin.

Ini ialah artikel ketiga dalam siri tentang cara penyahpepijat berfungsi. Anda harus membaca Bahagian 1 dan 2 sebelum membaca artikel ini.

Maklumat nyahpepijat

Penyusun moden boleh menukar kod bahasa peringkat tinggi dengan pelbagai aliran program berinden atau bersarang dan pembolehubah pelbagai jenis data kepada banyak data 0/1 yang dipanggil kod mesin Satu-satunya tujuan melakukan ini ialah Jalankan program pada CPU sasaran sebagai secepat mungkin. Secara umumnya, satu baris kod bahasa C boleh ditukar kepada beberapa baris kod mesin. Pembolehubah bertaburan di pelbagai bahagian kod mesin, sesetengahnya pada tindanan, sesetengahnya dalam daftar, atau terus dioptimumkan. Struktur data dan objek tidak "wujud" dalam kod mesin Ia hanya digunakan untuk menyimpan data ke dalam cache mengikut pengekodan struktur tertentu.

Jadi bagaimanakah penyahpepijat tahu di mana program harus berhenti apabila anda perlu berhenti seketika di pintu masuk fungsi tertentu? Bagaimanakah ia mengetahui bahawa apabila anda melihat nilai pembolehubah, bagaimana ia mendapati nilai itu? Jawapannya ialah, menyahpepijat maklumat.

Pengkompil juga akan menjana maklumat penyahpepijatan yang sepadan apabila menjana kod mesin. Maklumat penyahpepijatan mewakili hubungan antara program boleh laku dan kod sumber, dan disimpan bersama kod mesin dalam format yang ditakrifkan terlebih dahulu. Selama bertahun-tahun, banyak format untuk menyimpan maklumat ini telah dicipta untuk platform yang berbeza dan fail boleh laku. Walau bagaimanapun, artikel ini tidak akan masuk ke dalam sejarah format ini, tetapi sebaliknya bagaimana maklumat penyahpepijatan ini berfungsi, jadi kami akan menumpukan pada sesuatu seperti DWARF. DWARF kini digunakan secara meluas sebagai format penyahpepijatan untuk fail boleh laku pada platform seperti Linux dan Unix.

DWARF dalam ELF

Analisis prinsip kerja penyahpepijat (3): Kajian mendalam tentang maklumat penyahpepijatan

Menurut penerangan Wikipedianya, walaupun DWARF direka bersama-sama dengan ELF (DWARF ialah piawaian terbuka yang dilancarkan oleh Jawatankuasa Piawaian DWARF. Ikon yang ditunjukkan di atas berasal dari tapak web ini.), DWARF juga boleh dibenamkan secara teori ke dalam fail boleh laku yang lain format.

DWARF ialah format kompleks yang menggunakan pengalaman bertahun-tahun dengan pelbagai format seni bina dan sistem pengendalian. Tepat kerana ia menyelesaikan masalah sukar untuk menjana maklumat nyahpepijat untuk mana-mana bahasa peringkat tinggi pada mana-mana platform dan ABI (Antara Muka Binari Aplikasi), ia juga mestilah kompleks. Jika anda ingin menerangkan DWARF dengan teliti, tidak cukup dengan hanya membaca artikel yang nipis ini Sejujurnya, saya tidak memahami sepenuhnya DWARF sehingga setiap butiran kecil, jadi saya tidak dapat menerangkannya dengan teliti (jika anda rasa. Jika anda berminat, terdapat beberapa sumber di akhir artikel yang boleh membantu anda. Adalah disyorkan untuk memulakan dengan tutorial DWARF). Dalam artikel ini saya akan membentangkan DWARF dengan cara yang mudah difahami untuk menggambarkan cara maklumat penyahpepijatan sebenarnya berfungsi.

Bahagian nyahpepijat dalam fail ELF

Mula-mula mari kita lihat di mana DWARF terletak dalam fail ELF. ELF

Percubaan kami dalam artikel ini akan menggunakan fail boleh laku yang dibina daripada fail sumber bahasa C ini dan disusun ke dalam tracedprog2:

#include <stdio.h>

void do_stuff(int my_arg)、
{
    int my_local = my_arg + 2;
    int i;

    for (i = 0; i 
<p>Gunakan perintah objdump -h untuk menyemak <ruby>pengepala bahagian<rt></rt> dalam fail boleh laku ELF Kita akan melihat beberapa bahagian bermula dengan .debug_. Ini adalah bahagian penyahpepijatan DWARF. </ruby>
</p>
<pre class="brush:php;toolbar:false">26 .debug_aranges 00000020  00000000  00000000  00001037
                 CONTENTS, READONLY, DEBUGGING
27 .debug_pubnames 00000028  00000000  00000000  00001057
                 CONTENTS, READONLY, DEBUGGING
28 .debug_info   000000cc  00000000  00000000  0000107f
                 CONTENTS, READONLY, DEBUGGING
29 .debug_abbrev 0000008a  00000000  00000000  0000114b
                 CONTENTS, READONLY, DEBUGGING
30 .debug_line   0000006b  00000000  00000000  000011d5
                 CONTENTS, READONLY, DEBUGGING
31 .debug_frame  00000044  00000000  00000000  00001240
                 CONTENTS, READONLY, DEBUGGING
32 .debug_str    000000ae  00000000  00000000  00001284
                 CONTENTS, READONLY, DEBUGGING
33 .debug_loc    00000058  00000000  00000000  00001332
                 CONTENTS, READONLY, DEBUGGING
Nombor pertama setiap bahagian mewakili saiz bahagian, dan nombor terakhir mewakili offset kedudukan mula bahagian ini daripada ELF. Penyahpepijat menggunakan maklumat ini untuk membaca bahagian daripada fail boleh laku.

Sekarang mari kita lihat beberapa contoh praktikal mencari maklumat penyahpepijatan yang berguna dalam DWARF.

Cari fungsi Salah satu tugas paling asas penyahpepijat ialah apabila kita menetapkan titik putus pada fungsi tertentu, penyahpepijat perlu boleh menjeda pada titik masuk. Untuk melakukan ini, beberapa pemetaan mesti diwujudkan antara nama fungsi dalam kod peringkat tinggi dan alamat tempat arahan fungsi bermula dalam kod mesin.

Untuk mendapatkan perhubungan pemetaan ini, kita boleh mencari bahagian .debug_info dalam DWARF. Sebelum kita menyelami, sedikit pengetahuan asas diperlukan. Setiap jenis penerangan dalam DWARF dipanggil Entri Maklumat Nyahpepijat (DIE). Setiap DIE mempunyai tag tentang jenis, sifatnya, dsb. DIE disambungkan antara satu sama lain melalui nod adik beradik atau nod anak, dan nilai atribut juga boleh menunjuk kepada DIE yang lain.

Jalankan arahan berikut:

objdump --dwarf=info tracedprog2
Fail keluaran agak panjang demi kemudahan, kami hanya menumpukan pada baris ini (mulai dari sini, saya akan menggantikan maklumat panjang yang tidak berguna dengan (...) untuk memudahkan penataan huruf):

: Abbrev Number: 5 (DW_TAG_subprogram)
       DW_AT_external    : 1
       DW_AT_name        : (...): do_stuff
       DW_AT_decl_file   : 1
       DW_AT_decl_line   : 4
       DW_AT_prototyped  : 1
       DW_AT_low_pc      : 0x8048604
       DW_AT_high_pc     : 0x804863e
       DW_AT_frame_base  : 0x0      (location list)
       DW_AT_sibling     : 

<b3>: Abbrev Number: 9 (DW_TAG_subprogram)
    <b4>   DW_AT_external    : 1
    <b5>   DW_AT_name        : (...): main
    <b9>   DW_AT_decl_file   : 1
    <ba>   DW_AT_decl_line   : 14
    <bb>   DW_AT_type        : 
    <bf>   DW_AT_low_pc      : 0x804863e
    <c3>   DW_AT_high_pc     : 0x804865a
    <c7>   DW_AT_frame_base  : 0x2c     (location list)
</c7></c3></bf></bb></ba></b9></b5></b4></b3>

上面的代码中有两个带有 DW_TAG_subprogram 标签的入口,在 DWARF 中这是对函数的指代。注意,这是两个节的入口,其中一个是 do_stuff 函数的入口,另一个是主(main)函数的入口。这些信息中有很多值得关注的属性,但其中最值得注意的是 DW_AT_low_pc。它代表了函数开始处程序指针的值(在 x86 平台上是 EIP)。此处 0x8048604 代表了 do_stuff 函数开始处的程序指针。下面我们将利用 objdump -d 命令对可执行文件进行反汇编。来看看这块地址中都有什么:

08048604 <do_stuff>:
 8048604:       55           push   ebp
 8048605:       89 e5        mov    ebp,esp
 8048607:       83 ec 28     sub    esp,0x28
 804860a:       8b 45 08     mov    eax,DWORD PTR [ebp+0x8]
 804860d:       83 c0 02     add    eax,0x2
 8048610:       89 45 f4     mov    DWORD PTR [ebp-0xc],eax
 8048613:       c7 45 (...)  mov    DWORD PTR [ebp-0x10],0x0
 804861a:       eb 18        jmp    8048634 <do_stuff>
 804861c:       b8 20 (...)  mov    eax,0x8048720
 8048621:       8b 55 f0     mov    edx,DWORD PTR [ebp-0x10]
 8048624:       89 54 24 04  mov    DWORD PTR [esp+0x4],edx
 8048628:       89 04 24     mov    DWORD PTR [esp],eax
 804862b:       e8 04 (...)  call   8048534 <printf>
 8048630:       83 45 f0 01  add    DWORD PTR [ebp-0x10],0x1
 8048634:       8b 45 f0     mov    eax,DWORD PTR [ebp-0x10]
 8048637:       3b 45 f4     cmp    eax,DWORD PTR [ebp-0xc]
 804863a:       7c e0        jl     804861c <do_stuff>
 804863c:       c9           leave
 804863d:       c3           ret
</do_stuff></printf></do_stuff></do_stuff>

显然,0x8048604 是 do_stuff 的开始地址,这样一来,调试器就可以建立函数与其在可执行文件中的位置间的映射关系。

查找变量

假设我们当前在 do_staff 函数中某个位置上设置断点停了下来。我们想通过调试器取得 my_local 这个变量的值。调试器怎么知道在哪里去找这个值呢?很显然这要比查找函数更为困难。变量可能存储在全局存储区、堆栈、甚至是寄存器中。此外,同名变量在不同的作用域中可能有着不同的值。调试信息必须能够反映所有的这些变化,当然,DWARF 就能做到。

我不会逐一去将每一种可能的状况,但我会以调试器在 do_stuff 函数中查找 my_local 变量的过程来举个例子。下面我们再看一遍 .debug_info 中 do_stuff 的每一个入口,这次连它的子入口也要一起看。

: Abbrev Number: 5 (DW_TAG_subprogram)
       DW_AT_external    : 1
       DW_AT_name        : (...): do_stuff
       DW_AT_decl_file   : 1
       DW_AT_decl_line   : 4
       DW_AT_prototyped  : 1
       DW_AT_low_pc      : 0x8048604
       DW_AT_high_pc     : 0x804863e
       DW_AT_frame_base  : 0x0      (location list)
       DW_AT_sibling     : 
 : Abbrev Number: 6 (DW_TAG_formal_parameter)
       DW_AT_name        : (...): my_arg
       DW_AT_decl_file   : 1
       DW_AT_decl_line   : 4
       DW_AT_type        : 
       DW_AT_location    : (...)       (DW_OP_fbreg: 0)
 : Abbrev Number: 7 (DW_TAG_variable)
       DW_AT_name        : (...): my_local
       DW_AT_decl_file   : 1
       DW_AT_decl_line   : 6
       DW_AT_type        : 
    <a3>   DW_AT_location    : (...)      (DW_OP_fbreg: -20)
<a6>: Abbrev Number: 8 (DW_TAG_variable)
    <a7>   DW_AT_name        : i
    <a9>   DW_AT_decl_file   : 1
    <aa>   DW_AT_decl_line   : 7
    <ab>   DW_AT_type        : 
    <af>   DW_AT_location    : (...)      (DW_OP_fbreg: -24)
</af></ab></aa></a9></a7></a6></a3>

看到每个入口处第一对尖括号中的数字了吗?这些是嵌套的等级,在上面的例子中,以 开头的入口是以 开头的子入口。因此我们得知 my_local 变量(以 DW_TAG_variable 标签标记)是 do_stuff 函数的局部变量。除此之外,调试器也需要知道变量的数据类型,这样才能正确的使用与显示变量。上面的例子中 my_local 的变量类型指向另一个 DIE 。如果使用 objdump 命令查看这个 DIE 的话,我们会发现它是一个有符号 4 字节整型数据。

而为了在实际运行的程序内存中查找变量的值,调试器需要使用到 DW_AT_location 属性。对于 my_local 而言,是 DW_OP_fbreg: -20。这个代码段的意思是说 my_local 存储在距离它所在函数起始地址偏移量为 -20 的地方。

do_stuff 函数的 DW_AT_frame_base 属性值为 0x0 (location list)。这意味着这个属性的值需要在 location list 中查找。下面我们来一起看看。

$ objdump --dwarf=loc tracedprog2

tracedprog2:     file format elf32-i386

Contents of the .debug_loc section:

    Offset   Begin    End      Expression
    00000000 08048604 08048605 (DW_OP_breg4: 4 )
    00000000 08048605 08048607 (DW_OP_breg4: 8 )
    00000000 08048607 0804863e (DW_OP_breg5: 8 )
    00000000 <end of list>
    0000002c 0804863e 0804863f (DW_OP_breg4: 4 )
    0000002c 0804863f 08048641 (DW_OP_breg4: 8 )
    0000002c 08048641 0804865a (DW_OP_breg5: 8 )
    0000002c <end of list>
</end></end>

我们需要关注的是第一列(do_stuff 函数的 DW_AT_frame_base 属性包含 location list 中 0x0 的偏移量。而 main 函数的相同属性包含 0x2c 的偏移量,这个偏移量是第二套地址列表的偏移量)。对于调试器可能定位到的每一个地址,它都会指定当前栈帧到变量间的偏移量,而这个偏移就是通过寄存器来计算的。对于 x86 平台而言,bpreg4 指向 esp,而 bpreg5 指向 ebp。

让我们再看看 do_stuff 函数的头几条指令。

08048604 <do_stuff>:
 8048604:       55          push   ebp
 8048605:       89 e5       mov    ebp,esp
 8048607:       83 ec 28    sub    esp,0x28
 804860a:       8b 45 08    mov    eax,DWORD PTR [ebp+0x8]
 804860d:       83 c0 02    add    eax,0x2
 8048610:       89 45 f4    mov    DWORD PTR [ebp-0xc],eax
</do_stuff>

只有当第二条指令执行后,ebp 寄存器才真正存储了有用的值。当然,前两条指令的基址是由上面所列出来的地址信息表计算出来的。一但 ebp 确定了,计算偏移量就十分方便了,因为尽管 esp 在操作堆栈的时候需要移动,但 ebp 作为栈底并不需要移动。

究竟我们应该去哪里找 my_local 的值呢?在 0x8048610 这块地址后, my_local 的值经过在 eax 中的计算后被存在了内存中,从这里开始我们才需要关注 my_local 的值。调试器会利用 DW_OP_breg5: 8 这个栈帧来查找。我们回想下,my_local 的 DW_AT_location 属性值为 DW_OP_fbreg: -20。所以应当从基址中 -20 ,同时由于 ebp 寄存器需要 +8,所以最终结果为 ebp - 12。现在再次查看反汇编代码,来看看数据从 eax 中被移动到哪里了。当然,这里 my_local 应当被存储在了 ebp - 12 的地址中。

查看行号

当我们谈到在调试信息寻找函数的时候,我们利用了些技巧。当调试 C 语言源代码并在某个函数出放置断点的时候,我们并不关注第一条“机器码”指令(函数的调用准备工作已经完成而局部变量还没有初始化)。我们真正关注的是函数的第一行“C 代码”。

这就是 DWARF 完全覆盖映射 C 源代码中的行与可执行文件中机器码地址的原因。下面是 .debug_line 节中所包含的内容,我们将其转换为可读的格式展示如下。

$ objdump --dwarf=decodedline tracedprog2

tracedprog2:     file format elf32-i386

Decoded dump of debug contents of section .debug_line:

CU: /home/eliben/eli/eliben-code/debugger/tracedprog2.c:
File name           Line number    Starting address
tracedprog2.c                5           0x8048604
tracedprog2.c                6           0x804860a
tracedprog2.c                9           0x8048613
tracedprog2.c               10           0x804861c
tracedprog2.c                9           0x8048630
tracedprog2.c               11           0x804863c
tracedprog2.c               15           0x804863e
tracedprog2.c               16           0x8048647
tracedprog2.c               17           0x8048653
tracedprog2.c               18           0x8048658

很容易就可以看出其中 C 源代码与反汇编代码之间的对应关系。第 5 行指向do_stuff 函数的入口,0x8040604。第 6 行,指向 0x804860a ,正是调试器在调试 do_stuff 函数时需要停下来的地方。这里已经完成了函数调用的准备工作。上面的这些信息形成了行号与地址间的双向映射关系。

  • 当在某一行设置断点的时候,调试器会利用这些信息去查找相应的地址来做断点工作(还记得上篇文章中的 int 3 指令吗?)
  • 当指令造成段错误时,调试器会利用这些信息来查看源代码中发生问题的行。
libdwarf - 用 DWARF 编程

尽管使用命令行工具来获得 DWARF 很有用,但这仍然不够易用。作为程序员,我们希望知道当我们需要这些调试信息时应当怎么编程来获取这些信息。

自然我们想到的第一种方法就是阅读 DWARF 规范并按规范操作阅读使用。有句话说的好,分析 HTML 应当使用库函数,永远不要手工分析。对于 DWARF 来说正是如此。DWARF 比 HTML 要复杂得多。上面所展示出来的只是冰山一角。更糟糕的是,在实际的目标文件中,大部分信息是以非常紧凑的压缩格式存储的,分析起来更加复杂(信息中的某些部分,例如位置信息与行号信息,在某些虚拟机下是以指令的方式编码的)。

所以我们要使用库来处理 DWARF。下面是两种我熟悉的主要的库(还有些不完整的库这里没有写)

  1. BFD (libbfd),包含了 objdump (对,就是这篇文章中我们一直在用的这货),ld(GNU 连接器)与 as(GNU 编译器)。BFD 主要用于 GNU binutils。
  2. libdwarf ,同它的哥哥 libelf 一同用于 Solaris 与 FreeBSD 中的调试信息分析。

相比较而言我更倾向于使用 libdwarf,因为我对它了解的更多,并且 libdwarf 的开源协议更开放(LGPL 对比 GPL)。

因为 libdwarf 本身相当复杂,操作起来需要相当多的代码,所以我在这不会展示所有代码。你可以在 这里 下载代码并运行试试。运行这些代码需要提前安装 libelfand 与 libdwarf ,同时在使用连接器的时候要使用参数 -lelf 与 -ldwarf。

这个示例程序可以接受可执行文件并打印其中的函数名称与函数入口地址。下面是我们整篇文章中使用的 C 程序经过示例程序处理后的输出。

$ dwarf_get_func_addr tracedprog2
DW_TAG_subprogram: 'do_stuff'
low pc  : 0x08048604
high pc : 0x0804863e
DW_TAG_subprogram: 'main'
low pc  : 0x0804863e
high pc : 0x0804865a

libdwarf 的文档很棒,如果你花些功夫,利用 libdwarf 获得这篇文章中所涉及到的 DWARF 信息应该并不困难。

结论与计划

原理上讲,调试信息是个很简单的概念。尽管实现细节可能比较复杂,但经过了上面的学习我想你应该了解了调试器是如何从可执行文件中获取它需要的源代码信息的了。对于程序员而言,程序只是代码段与数据结构;对可执行文件而言,程序只是一系列存储在内存或寄存器中的指令或数据。但利用调试信息,调试器就可以将这两者连接起来,从而完成调试工作。

此文与这系列的前两篇,一同介绍了调试器的内部工作过程。利用这里所讲到的知识,再敲些代码,应该可以完成一个 Linux 中最简单、基础但也有一定功能的调试器。

下一步我并不确定要做什么,这个系列文章可能就此结束,也有可能我要讲些堆栈调用的事情,又或者讲 Windows 下的调试。你们有什么好的点子或者相关材料,可以直接评论或者发邮件给我。

参考
  • objdump 参考手册
  • ELF 与 DWARF 的维基百科
  • Dwarf Debugging Standard 主页,这里有很棒的 DWARF 教程与 DWARF 标准,作者是 Michael Eager。第二版基于 GCC 也许更能吸引你。
  • libdwarf 主页,这里可以下载到 libwarf 的完整库与参考手册
  • BFD 文档


Atas ialah kandungan terperinci Analisis prinsip kerja penyahpepijat (3): Kajian mendalam tentang maklumat penyahpepijatan. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Artikel ini dikembalikan pada:linuxprobe.com. Jika ada pelanggaran, sila hubungi admin@php.cn Padam