开发一个Linux调试器(九):处理变量

简介:

开发一个Linux调试器(九):处理变量

变量是偷偷摸摸的。有时,它们会很高兴地呆在寄存器中,但是一转头就会跑到堆栈中。为了优化,编译器可能会完全将它们从窗口中抛出。无论变量在内存中的如何移动,我们都需要一些方法在调试器中跟踪和操作它们。这篇文章将会教你如何处理调试器中的变量,并使用 libelfin 演示一个简单的实现。

系列文章索引

  1. 准备环境
  2. 断点
  3. 寄存器和内存
  4. ELF 和 DWARF
  5. 源码和信号
  6. 源码级逐步执行
  7. 源码级断点
  8. 堆栈展开
  9. 处理变量
  10. 高级话题

在开始之前,请确保你使用的 libelfin 版本是我分支上的 fbreg。这包含了一些 hack 来支持获取当前堆栈帧的基址并评估位置列表,这些都不是由原生的 libelfin 提供的。你可能需要给 GCC 传递 -gdwarf-2 参数使其生成兼容的 DWARF 信息。但是在实现之前,我将详细说明 DWARF 5 最新规范中的位置编码方式。如果你想要了解更多信息,那么你可以从这里获取该标准。

DWARF 位置

某一给定时刻的内存中变量的位置使用 DW_AT_location 属性编码在 DWARF 信息中。位置描述可以是单个位置描述、复合位置描述或位置列表。

  • 简单位置描述:描述了对象的一个连续的部分(通常是所有部分)的位置。简单位置描述可以描述可寻址存储器或寄存器中的位置,或缺少位置(具有或不具有已知值)。比如,DW_OP_fbreg -32: 一个整个存储的变量 - 从堆栈帧基址开始的32个字节。
  • 复合位置描述:根据片段描述对象,每个对象可以包含在寄存器的一部分中或存储在与其他片段无关的存储器位置中。比如, DW_OP_reg3 DW_OP_piece 4 DW_OP_reg10 DW_OP_piece 2:前四个字节位于寄存器 3 中,后两个字节位于寄存器 10 中的一个变量。
  • 位置列表:描述了具有有限生存期或在生存期内更改位置的对象。比如:
    • <loclist with 3 entries follows>
      • [ 0]<lowpc=0x2e00><highpc=0x2e19>DW_OP_reg0
      • [ 1]<lowpc=0x2e19><highpc=0x2e3f>DW_OP_reg3
      • [ 2]<lowpc=0x2ec4><highpc=0x2ec7>DW_OP_reg2
    • 根据程序计数器的当前值,位置在寄存器之间移动的变量。

根据位置描述的种类,DW_AT_location 以三种不同的方式进行编码。exprloc 编码简单和复合的位置描述。它们由一个字节长度组成,后跟一个 DWARF 表达式或位置描述。loclist 和 loclistptr 的编码位置列表,它们在 .debug_loclists 部分中提供索引或偏移量,该部分描述了实际的位置列表。

DWARF 表达式

使用 DWARF 表达式计算变量的实际位置。这包括操作堆栈值的一系列操作。有很多 DWARF 操作可用,所以我不会详细解释它们。相反,我会从每一个表达式中给出一些例子,给你一个可用的东西。另外,不要害怕这些;libelfin 将为我们处理所有这些复杂性。

  • 字面编码
    • DW_OP_lit0、DW_OP_lit1……DW_OP_lit31
      • 将字面量压入堆栈
    • DW_OP_addr <addr>
      • 将地址操作数压入堆栈
    • DW_OP_constu <unsigned>
      • 将无符号值压入堆栈
  • 寄存器值
    • DW_OP_fbreg <offset>
      • 压入在堆栈帧基址找到的值,偏移给定值
    • DW_OP_breg0、DW_OP_breg1…… DW_OP_breg31 <offset>
      • 将给定寄存器的内容加上给定的偏移量压入堆栈
  • 堆栈操作
    • DW_OP_dup
      • 复制堆栈顶部的值
    • DW_OP_deref
      • 将堆栈顶部视为内存地址,并将其替换为该地址的内容
  • 算术和逻辑运算
    • DW_OP_and
      • 弹出堆栈顶部的两个值,并压回它们的逻辑 AND
    • DW_OP_plus
      • 与 DW_OP_and 相同,但是会添加值
  • 控制流操作
    • DW_OP_le、DW_OP_eq、DW_OP_gt 等
      • 弹出前两个值,比较它们,并且如果条件为真,则压入 1,否则为 0
    • DW_OP_bra <offset>
      • 条件分支:如果堆栈的顶部不是 0,则通过 offset 在表达式中向后或向后跳过
  • 输入转化
    • DW_OP_convert <DIE offset>
      • 将堆栈顶部的值转换为不同的类型,它由给定偏移量的 DWARF 信息条目描述
  • 特殊操作
    • DW_OP_nop
      • 什么都不做!

DWARF 类型

DWARF 类型的表示需要足够强大来为调试器用户提供有用的变量表示。用户经常希望能够在应用程序级别进行调试,而不是在机器级别进行调试,并且他们需要了解他们的变量正在做什么。

DWARF 类型与大多数其他调试信息一起编码在 DIE 中。它们可以具有指示其名称、编码、大小、字节等的属性。无数的类型标签可用于表示指针、数组、结构体、typedef 以及 C 或 C++ 程序中可以看到的任何其他内容。

以这个简单的结构体为例:


  
  
  1. struct test{ 
  2.     int i; 
  3.     float j; 
  4.     int k[42]; 
  5.     test* next
  6. }; 

这个结构体的父 DIE 是这样的:


  
  
  1. < 1><0x0000002a>    DW_TAG_structure_type 
  2.                       DW_AT_name                  "test" 
  3.                       DW_AT_byte_size             0x000000b8 
  4.                       DW_AT_decl_file             0x00000001 test.cpp 
  5.                       DW_AT_decl_line             0x00000001 

上面说的是我们有一个叫做 test 的结构体,大小为 0xb8,在 test.cpp 的第 1 行声明。接下来有许多描述成员的子 DIE。


  
  
  1. < 2><0x00000032>      DW_TAG_member 
  2.                         DW_AT_name                  "i" 
  3.                         DW_AT_type                  <0x00000063> 
  4.                         DW_AT_decl_file             0x00000001 test.cpp 
  5.                         DW_AT_decl_line             0x00000002 
  6.                         DW_AT_data_member_location  0 
  7. < 2><0x0000003e>      DW_TAG_member 
  8.                         DW_AT_name                  "j" 
  9.                         DW_AT_type                  <0x0000006a> 
  10.                         DW_AT_decl_file             0x00000001 test.cpp 
  11.                         DW_AT_decl_line             0x00000003 
  12.                         DW_AT_data_member_location  4 
  13. < 2><0x0000004a>      DW_TAG_member 
  14.                         DW_AT_name                  "k" 
  15.                         DW_AT_type                  <0x00000071> 
  16.                         DW_AT_decl_file             0x00000001 test.cpp 
  17.                         DW_AT_decl_line             0x00000004 
  18.                         DW_AT_data_member_location  8 
  19. < 2><0x00000056>      DW_TAG_member 
  20.                         DW_AT_name                  "next" 
  21.                         DW_AT_type                  <0x00000084> 
  22.                         DW_AT_decl_file             0x00000001 test.cpp 
  23.                         DW_AT_decl_line             0x00000005 
  24.                         DW_AT_data_member_location  176(as signed = -80) 

每个成员都有一个名称、一个类型(它是一个 DIE 偏移量)、一个声明文件和行,以及一个指向其成员所在的结构体的字节偏移。其类型指向如下。


  
  
  1. < 1><0x00000063>    DW_TAG_base_type 
  2.                       DW_AT_name                  "int" 
  3.                       DW_AT_encoding              DW_ATE_signed 
  4.                       DW_AT_byte_size             0x00000004 
  5. < 1><0x0000006a>    DW_TAG_base_type 
  6.                       DW_AT_name                  "float" 
  7.                       DW_AT_encoding              DW_ATE_float 
  8.                       DW_AT_byte_size             0x00000004 
  9. < 1><0x00000071>    DW_TAG_array_type 
  10.                       DW_AT_type                  <0x00000063> 
  11. < 2><0x00000076>      DW_TAG_subrange_type 
  12.                         DW_AT_type                  <0x0000007d> 
  13.                         DW_AT_count                 0x0000002a 
  14. < 1><0x0000007d>    DW_TAG_base_type 
  15.                       DW_AT_name                  "sizetype" 
  16.                       DW_AT_byte_size             0x00000008 
  17.                       DW_AT_encoding              DW_ATE_unsigned 
  18. < 1><0x00000084>    DW_TAG_pointer_type 
  19.                       DW_AT_type                  <0x0000002a> 

如你所见,我笔记本电脑上的 int 是一个 4 字节的有符号整数类型,float是一个 4 字节的浮点数。整数数组类型通过指向 int 类型作为其元素类型,sizetype(可以认为是 size_t)作为索引类型,它具有 2a 个元素。 test * 类型是 DW_TAG_pointer_type,它引用 test DIE。

实现简单的变量读取器

如上所述,libelfin 将为我们处理大部分复杂性。但是,它并没有实现用于表示可变位置的所有方法,并且在我们的代码中处理这些将变得非常复杂。因此,我现在选择只支持 exprloc。请根据需要添加对更多类型表达式的支持。如果你真的有勇气,请提交补丁到 libelfin 中来帮助完成必要的支持!

处理变量主要是将不同部分定位在存储器或寄存器中,读取或写入与之前一样。为了简单起见,我只会告诉你如何实现读取。

首先我们需要告诉 libelfin 如何从我们的进程中读取寄存器。我们创建一个继承自 expr_context 的类并使用 ptrace 来处理所有内容:


  
  
  1. class ptrace_expr_context : public dwarf::expr_context { 
  2. public
  3.     ptrace_expr_context (pid_t pid) : m_pid{pid} {} 
  4.     dwarf::taddr reg (unsigned regnum) override { 
  5.         return get_register_value_from_dwarf_register(m_pid, regnum); 
  6.     } 
  7.     dwarf::taddr pc() override { 
  8.         struct user_regs_struct regs; 
  9.         ptrace(PTRACE_GETREGS, m_pid, nullptr, &regs); 
  10.         return regs.rip; 
  11.     } 
  12.     dwarf::taddr deref_size (dwarf::taddr address, unsigned size) override { 
  13.         //TODO take into account size 
  14.         return ptrace(PTRACE_PEEKDATA, m_pid, address, nullptr); 
  15.     } 
  16. private: 
  17.     pid_t m_pid; 
  18. }; 

读取将由我们 debugger 类中的 read_variables 函数处理:


  
  
  1. void debugger::read_variables() { 
  2.     using namespace dwarf; 
  3.     auto func = get_function_from_pc(get_pc()); 
  4.     //... 

我们上面做的第一件事是找到我们目前进入的函数,然后我们需要循环访问该函数中的条目来寻找变量:


  
  
  1. for (const auto& die : func) { 
  2.     if (die.tag == DW_TAG::variable) { 
  3.         //... 
  4.     } 

我们通过查找 DIE 中的 DW_AT_location 条目获取位置信息:


  
  
  1. auto loc_val = die[DW_AT::location]; 

接着我们确保它是一个 exprloc,并请求 libelfin 来评估我们的表达式:


  
  
  1. if (loc_val.get_type() == value::type::exprloc) { 
  2.     ptrace_expr_context context {m_pid}; 
  3.     auto result = loc_val.as_exprloc().evaluate(&context); 

现在我们已经评估了表达式,我们需要读取变量的内容。它可以在内存或寄存器中,因此我们将处理这两种情况:


  
  
  1. switch (result.location_type) { 
  2.                 case expr_result::type::address: 
  3.                 { 
  4.                     auto value = read_memory(result.value); 
  5.                     std::cout << at_name(die) << " (0x" << std::hex << result.value << ") = " 
  6.                               << value << std::endl; 
  7.                     break; 
  8.                 } 
  9.                 case expr_result::type::reg: 
  10.                 { 
  11.                     auto value = get_register_value_from_dwarf_register(m_pid, result.value); 
  12.                     std::cout << at_name(die) << " (reg " << result.value << ") = " 
  13.                               << value << std::endl; 
  14.                     break; 
  15.                 } 
  16.                 default
  17.                     throw std::runtime_error{"Unhandled variable location"}; 
  18.                 } 

你可以看到,我根据变量的类型,打印输出了值而没有解释。希望通过这个代码,你可以看到如何支持编写变量,或者用给定的名字搜索变量。

最后我们可以将它添加到我们的命令解析器中:


  
  
  1. else if(is_prefix(command, "variables")) { 
  2.     read_variables(); 

测试一下

编写一些具有一些变量的小功能,不用优化并带有调试信息编译它,然后查看是否可以读取变量的值。尝试写入存储变量的内存地址,并查看程序改变的行为。

已经有九篇文章了,还剩最后一篇!下一次我会讨论一些你可能会感兴趣的更高级的概念。现在你可以在这里找到这个帖子的代码。


原文发布时间为:2017-10-09

本文作者:Simon Brand

本文来自云栖社区合作伙伴“51CTO”,了解相关信息可以关注。

相关文章
|
2月前
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
106 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
2月前
|
安全 Linux
Linux线程(十一)线程互斥锁-条件变量详解
Linux线程(十一)线程互斥锁-条件变量详解
|
3月前
|
存储 Linux 开发工具
如何进行Linux内核开发【ChatGPT】
如何进行Linux内核开发【ChatGPT】
|
4月前
|
Java Linux API
Linux设备驱动开发详解2
Linux设备驱动开发详解
51 6
|
4月前
|
消息中间件 算法 Unix
Linux设备驱动开发详解1
Linux设备驱动开发详解
55 5
|
4月前
|
编解码 安全 Linux
基于arm64架构国产操作系统|Linux下的RTMP|RTSP低延时直播播放器开发探究
这段内容讲述了国产操作系统背景下,大牛直播SDK针对国产操作系统与Linux平台发布的RTMP/RTSP直播播放SDK。此SDK支持arm64架构,基于X协议输出视频,采用PulseAudio和Alsa Lib处理音频,具备实时静音、快照、缓冲时间设定等功能,并支持H.265编码格式。此外,提供了示例代码展示如何实现多实例播放器的创建与管理,包括窗口布局调整、事件监听、视频分辨率变化和实时快照回调等关键功能。这一技术实现有助于提高直播服务的稳定性和响应速度,适应国产操作系统在各行业中的应用需求。
126 3
|
5月前
|
Java Linux
linux 对子用户配置java 环境变量
linux 对子用户配置java 环境变量
43 3
|
4月前
|
Linux Shell
在Linux中,如何将二进制文件添加到 $PATH 变量中?
在Linux中,如何将二进制文件添加到 $PATH 变量中?
|
5月前
|
Ubuntu Linux Docker
Java演进问题之Alpine Linux创建更小的Docker镜像如何解决
Java演进问题之Alpine Linux创建更小的Docker镜像如何解决
|
Shell Linux
Linux各种变量的含义
# 是传给脚本的参数个数0 是脚本本身的名字1 是传递给该shell脚本的第一个参数2 是传递给该shell脚本的第二个参数@ 是传给脚本的所有参数的列表* 是以一个单字符串显示所有向脚本传递的参数,与位置变量不同,参数可超过9个 $$ 是脚本运行的当...
944 0