首页
社区
课程
招聘
[翻译]自己动手编写一个Linux调试器系列之3 寄存器和内存 by lantie@15PB
发表于: 2017-10-15 18:42 6987

[翻译]自己动手编写一个Linux调试器系列之3 寄存器和内存 by lantie@15PB

2017-10-15 18:42
6987

在上一篇文章中,我们向调试器添加了简单的地址断点。这一次,我们将添加读取寄存器和内存的功能,有了这个功能我们就可以观察寄存器状态和利用程序计数器(RIP)改变程序的执行流程了。

在我们编写读取寄存器代码之前,首先需要确定调试器支持什么平台,我们选择x86_64(即64位)。除了通用寄存器和专用寄存器之外,x86_64还提供了浮点寄存器和向量寄存器。为了简单起见,我将省略后两者,但如果你愿意,可以选择支持它们。x86_64还允许你访问的一些64位寄存器作为32位、16位、8位寄存器访问,但在这里我只支持64位。由于这样的简化,对于每个寄存器,我们只需要保存其名称,其DWARF寄存器编号和从ptrace返回的结构里的位置即可。我定义了一个枚举来引用寄存器,然后我编写了一个全局局存起描述符数组,其中元素的顺序与ptrace返回的寄存器结构中的顺序相同。

如果你想自己查看寄存器的数据结构可以在/usr/include/sys/user.h中找到,DWARF寄存器编号取自System V x86_64 ABI

现在我们可以编写一连串的函数来与寄存器进行交互。我们希望能读取寄存器、修改寄存器,从DWARF寄存器编号中检索一个值,并按名称查找寄存器,反之亦然。我们从get_register_value开始:

又一次,ptrace让我们轻松访问到了想要的数据。我们只是构造一个user_regs_struct的实例,并将PTRACE_GETREGS参数传入了ptrace就可以完成。

现在我们要根据请求的寄存器来读取regs。我们可以写一个大的switch语句,但是由于我们按照与user_regs_struct相同的顺序排列了我们的g_register_descriptors表,所以我们可以检索寄存器描述符的索引,并将user_regs_struct作为 uint64_t类型的数组访问。[注解1]

由于user_regs_struct是一个标准的布局类型(线性结构),所以转为 uint64_t是安全的,但我认为指针计算在技术上是比较难看的。由于目前编译器还没有警告,再加上我也比较懒,所以就先这样做,但是如果您想保持最大的正确性,那么就编写一个大的switch语句吧。

set_register_value是一样的,我们只需要获取位置,并在其位置上写入寄存器的值:

接下来是通过DWARF寄存器号查找。 这一次我会检查一个错误条件,以防万一我们得到一些奇怪的DWARF信息:

到这几乎完成,现在还有对注册的寄存器名称的查找:

最后,我们将添加一个简单的函数来转储所有寄存器的内容:

如你所见,iostreams有一个非常简洁的接口,可以很好地输出十六进制数据。
如果你喜欢的话,可以自由地对I/O输出做格式控制。[注解2]

这给了我们足够的支持来在调试器的其余部分轻松地处理寄存器,因此我们现在可以将它添加到我们的UI中。

我们需要在这里做的就是向handle_command函数添加一个新命令。使用以下代码,用户将能够键入register read raxregister write rax 0x42,等等。

在设置断点时,我们已经从内存中读取和写入内存,因此只需添加一些函数来隐藏ptrace调用即可。

你可能想要一次添加对读取和写入的支持,通过每次你想读另一个单词时递增地址即可。您还可以使用process_vm_readvprocess_vm_writev/ proc/<pid>/mem而不是ptrace
现在我们将为UI添加命令:

在测试我们的更改之前,我们现在可以执行一个更合理的版本的continue_execution。 由于我们可以得到程序计数器(RIP),所以可以检查我们的断点映射,看看我们是否处于断点。 如果是这样,我们可以在继续之前禁用断点并重新切断它。

首先,为了清晰简洁,我们将添加几个帮助函数:

然后我们可以写一个函数来跳过一个断点:

首先,我们检查是否为当前PC的值设置了一个断点。 如果有的话,我们先把执行返回到断点之前,禁用它,重新执行原来的指令,然后再重新启用断点。

wait_for_signal将封装我们通常的waitpid模式:

最后我们重写如下的continue_execution

现在我们可以读取和修改寄存器,可以使用我们的hello world程序进行调试测试。 作为第一个测试,请尝试再次在调用指令上设置断点,并从中继续。 你应该看到Hello world被打印出来。 有趣的部分在输出调用之后设置一个断点,继续运行程序,然后将调用参数设置代码的地址写入程序计数器(rip)并继续。 由于这个程序计数器的修改,你应该再次看到Hello world被打印了。 为了防止你不确定断点的位置,以下是我最后一篇文章的objdump输出:

你将要将程序计数器移回0x40093a,以便正确设置esi和edi寄存器。

在下一篇文章中,我们将首先介绍DWARF信息,并在调试器中添加各种单步。 之后,我们编写的工具将拥有调试器的主要功能,我们可以通过单步代码,设置断点,修改数据等等使用工具。 和往常一样,如果您有任何疑问,请在下方发表评论!

你可以在这里找到这篇文章的代码

注解1:你也可以重新排序寄存器表,并将其转换为基础类型以用作索引,但是我以现在的方式编写了,懒得了改变它了。
注解2:哈哈哈哈哈哈哈哈

吐槽一下:原来原作者也比较懒哈哈~

原文来自:https://blog.tartanllama.xyz/writing-a-linux-debugger-registers/
翻译来自:lantie@15PB 专注于信息安全教育 http://www.15pb.com.cn
到此原文结束,以下是实践部分

这一节,最大的收获就是设计了寄存器的结构,能够读取寄存器信息,并且可以修改程序计数器(RIP)完成程序下断点之后的恢复执行。调试器到目前已经实现了以下命令:

使用自己的调试器,感受一下效果o( ̄︶ ̄)o。
首先,使用命令objdump -d hello15pb|grep main查看例子程序hello15pb的main函数地址。记下main函数附近的第一个地址,方便设置断点。

查看地址

然后使用编译好的调试,调试hello15pb,使用命令break <地址>main函数上设置断点

设置断点

然后使用命令cont运行程序,此时程序应该会暂停到main函数后一个字节上。使用命令register read rip可以查看当前程序计数器的内容。

查看寄存器的值

还可以使用命令register dump查看所有寄存器的信息

查看所有寄存器

并可以使用命令register write r15 1,修改指定寄存器r15的值为1。

修改寄存器

最后当使用命令cont时,程序会将断点恢复,并将rip减一,执行被断点覆盖的指令,程序就会正常的跑起来了!哈哈,15PB的同学们加油了!

正常运行

由于调试器的代码使用的是最新的C++ 14的标准编写了,其中使用了一些新特性,所以如果编译器版本太低,会导致编译无法通过,所以需要修改一些代码将其变为老的方式,比如我遇到的了一个问题是unordered_map容器的使用,上一节就遇到了问题,这一节继续发生问题。所以想要将代码编译通过,需要将编译器升级到最高,在这里我已经将ubuntu系统上的gcc升级到了5.2,但经过测试还是不行,不知何原因,最后只能修改代码才完成通过编译,大家在编译的时候,注意以上问题吧!
我将代码经过比较大的重构,将原先的代码重新组织和编排,修改了编译时unordered_map容器操作的问题。

问题

我重构之后的Clion项目工程:

 
 
 
 
 
 
 
 
 
 

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

上传的附件:
收藏
免费 1
支持
分享
最新回复 (7)
雪    币: 7012
活跃值: (4222)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2017-10-16 18:06
0
雪    币: 35
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
2017-10-20 09:00
0
雪    币: 49
活跃值: (118)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
支持薛老师
2017-10-20 10:15
0
雪    币: 6
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
期待薛老师的原创内容。
2017-11-2 15:06
0
雪    币: 371
活跃值: (94)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
6
bp.enable();//这个断点已经用过了,为啥还要设置为bp.enable呢?求薛老师指点
void  debugger::step_over_breakpoint()  {
        //  -  1  because  execution  will  go  past  the  breakpoint
        auto  possible_breakpoint_location  =  get_pc()  -  1;
 
        if  (m_breakpoints.count(possible_breakpoint_location))  {
                auto&  bp  =  m_breakpoints[possible_breakpoint_location];
 
                if  (bp.is_enabled())  {
                        auto  previous_instruction_address  =  possible_breakpoint_location;
                        set_pc(previous_instruction_address);
 
                        bp.disable();
                        ptrace(PTRACE_SINGLESTEP,  m_pid,  nullptr,  nullptr);
                        wait_for_signal();
                        bp.enable();//这个断点已经用过了,为啥还要设置为bp.enable呢?求薛老师指点
                }
        }
}
2017-11-14 17:33
0
雪    币: 486
活跃值: (489)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
7
darmao bp.enable();//这个断点已经用过了,为啥还要设置为bp.enable呢?求薛老师指点 void debugger::step_over_breakpoint() { // - ...
@darmao,  已经用过了断点,但执行过好,想要再次触发,需要再次恢复断点,也就是再次设置断点。
2017-11-15 08:45
0
雪    币: 371
活跃值: (94)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
8
蓝铁 @darmao, 已经用过了断点,但执行过好,想要再次触发,需要再次恢复断点,也就是再次设置断点。
明白了,也就是在循环的时候打个断点,每次进入这个循环都能在这里断下来。。之前没有考虑循环,谢谢老师的指点
2017-11-15 17:42
0
游客
登录 | 注册 方可回帖
返回
//