debugger(五):source level stepping

2024-06-11 19:52
文章标签 level source debugger stepping

本文主要是介绍debugger(五):source level stepping,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

〇、前言

前面的源代码打印,利用了 DWARF 格式化的信息,现在我们更进一步,利用它分别进行 stepistep_overstep_instep_out

一、stepi

这个最简单,我们只需要利用 ptrace 就行:

void Debugger::single_step_instruction() {ptrace(PTRACE_SINGLESTEP, m_pid, nullptr, nullptr);wait_for_signal();
}void Debugger::single_step_instruction_with_breakpoint_check() {if (m_breakpoints.count(get_pc())) {step_over_breakpoint();}else {single_step_instruction();}
}void Debugger::step_over_breakpoint() {// 二次检查if (m_breakPoints.count(get_pc())) {auto &bp = m_breakPoints[get_pc()];if (bp.is_enabled()) {bp.disable();ptrace(PTRACE_SINGLESTEP, m_pid, nullptr, nullptr);wait_for_signal();bp.enable();}}
}

这里需要注意的是,如果改变了被跟踪程序的状态,必须要调用 wait(),这是因为我们必须要同步一些信息,比如 pc等寄存器,不然就会保错。

二、step_out

要完成这个函数,我们需要对栈帧所有了解。当函数执行到一个函数之中时,比如:

void f() {int foo = 1;		<-----执行在此处int foo1 = 1;e();int foo2 = 1;int foo3 = 1;
}int main() {int foo = 1;int foo1 = 1;int foo2 = 1;int foo3 = 1;f();int foo4 = 1;
}

main() 函数进入 f() 的时候,f() 首先会完成 prologue,新的栈帧是 f() 自己创建的。

函数调用过程

  1. Prologue:函数调用开始时,调用者函数(例如 main())会将当前函数的返回地址压入栈中(事实上,这是在 CALL 中隐式执行的),然后执行被调用函数(例如 f())的 prologue 部分。Prologue 是被调用函数的一部分,用于准备函数的栈帧和执行环境。

  2. 栈帧创建:在 prologue 部分,被调用函数会为自己创建一个新的栈帧。栈帧包含了函数的局部变量、参数、返回地址等信息。这个栈帧通常位于栈上,所以在创建时需要适当地调整栈指针。

  3. 保存寄存器状态:在 prologue 部分,被调用函数还可能需要保存调用前的寄存器状态,以便在函数结束后恢复。

  4. 函数执行:被调用函数开始执行其主体部分,执行其中的语句和操作。

  5. Epilogue:函数执行完毕后,执行 epilogue 部分。Epilogue 用于清理栈帧和恢复调用前的环境,包括恢复寄存器状态和返回地址。

栈帧布局

  1. 局部变量分配:在栈帧中,局部变量通常位于栈顶的一段区域。在调用函数的 prologue 部分,会为局部变量分配空间。

  2. 参数传递:函数参数可以通过栈传递,也可以通过寄存器传递(特别是在寄存器架构中)。参数通常存储在栈帧的特定位置,被调用函数在开始时会从这些位置读取参数。

  3. 返回地址:返回地址是在调用函数 prologue 部分压入栈中的,它指示了在函数执行完毕后应该返回到哪里继续执行。

  4. 其他信息:栈帧还可能包含其他的信息,如前一个函数的栈帧指针、异常处理相关信息等。

其它情况

  • 递归调用:每次函数调用都会创建一个新的栈帧,如果函数递归调用自身,会导致多个栈帧同时存在于栈上。

  • 内联函数:内联函数在调用时不会创建新的栈帧,而是将函数的内容嵌入到调用者函数中,从而减少了函数调用的开销。

  • 优化技术:编译器会对函数栈帧进行优化,例如将局部变量寄存器化、使用栈帧重用等,以提高程序的性能和效率。

比如这段代码:

#1  0x000055555555526d in main () at /home/luyoung/mydebugger/examples/stack.cpp:53
(gdb) disassemble 
Dump of assembler code for function _Z1fv:0x0000555555555210 <+0>:     endbr64 0x0000555555555214 <+4>:     push   %rbp0x0000555555555215 <+5>:     mov    %rsp,%rbp0x0000555555555218 <+8>:     sub    $0x10,%rsp
=> 0x000055555555521c <+12>:    movl   $0x1,-0x10(%rbp)0x0000555555555223 <+19>:    movl   $0x1,-0xc(%rbp)0x000055555555522a <+26>:    call   0x5555555551e0 <_Z1ev>0x000055555555522f <+31>:    movl   $0x1,-0x8(%rbp)0x0000555555555236 <+38>:    movl   $0x1,-0x4(%rbp)0x000055555555523d <+45>:    nop0x000055555555523e <+46>:    leave  0x000055555555523f <+47>:    ret    
End of assembler dump.

对应于:

void f() {int foo = 1;		<-----执行在此处int foo1 = 1;e();int foo2 = 1;int foo3 = 1;
}

这就是 main() 进入 f() 后所做的工作:

  • endbr64:这是一个指令,用于指示处理器启用 64 位模式下的 endbr 指令。

  • push %rbp:将当前栈帧main() 的基址指针(Frame Pointer, RBP)压入栈中,以便后续函数使用。

  • mov %rsp,%rbp:将栈顶指针(Stack Pointer, RSP)的值赋给基址指针(RBP),建立当前函数的栈帧。

  • sub $0x10,%rsp:在栈上分配 16 字节的空间,用于存储局部变量或临时数据,更新 rsp。

  • movl $0x1,-0x10(%rbp):将立即数 1 存储到相对于基址指针(RBP)偏移量为 -0x10 的内存位置。

  • movl $0x1,-0xc(%rbp):将立即数 1 存储到相对于基址指针(RBP)偏移量为 -0xc 的内存位置。

  • call 0x5555555551b0 <_Z1dv>:调用地址为 0x5555555551b0 的函数 _Z1dv,这个函数可能接受参数并返回结果。

  • movl $0x1,-0x8(%rbp):将立即数 1 存储到相对于基址指针(RBP)偏移量为 -0x8 的内存位置。

  • movl $0x1,-0x4(%rbp):将立即数 1 存储到相对于基址指针(RBP)偏移量为 -0x4 的内存位置。

  • nop:空操作指令,不执行任何操作。

  • leave:恢复栈帧,将栈帧移出栈。

  • ret:返回指令,从当前函数返回到调用者函数。

因此我们想要跳出函数 f(),得知晓这个函数的返回地址,它放在哪里呢?

返回地址实际上是在调用函数之前的步骤中由调用指令(如 call)隐式处理的,它会将下一条指令的地址(即函数 f() 的地址)压入栈中,作为返回时应该跳转的地址。这里就很清晰了,因为在这之后,紧接着入栈的是 rbp,接着是把 rsp 放入 rbp,当前 rbp 中值就是那时候的 rsp,也是新的栈帧。我们只要把 rbp+8,就能得到它了。

void Debugger::step_out() {auto frame_pointer = get_register_value(m_pid, reg::rbp);auto return_address = read_memory(frame_pointer+8);bool should_remove_breakpoint = false;if (!m_breakpoints.count(return_address)) {set_breakpoint_at_address(return_address);should_remove_breakpoint = true;}continue_execution();if (should_remove_breakpoint) {remove_breakpoint(return_address);

这里有一个细节,就是得判断返回地址是不是一个断点,如果是,那就不用管,继续执行之后它自动会停在返回处;如果不是,那就要手动打断点,单步执行之后它就回停在那里,接着将断点取消,就可以了。

三、step_in()

这个比较简单,假设在函数直行到:

void f() {int foo = 1;		<-----执行在此处int foo1 = 1;e();int foo2 = 1;int foo3 = 1;
}

那么 step_in 会继续执行,直行到 e() 的时候,会跳进去,我们只需要通过当前 pc来获取行号,然后单步执行,一直到行号变化为止。

void Debugger::step_in() {auto line = get_line_entry_from_pc(get_offset_pc())->line;while (get_line_entry_from_pc(get_offset_pc())->line == line) {single_step_instruction_with_breakpoint_check();}auto line_entry = get_line_entry_from_pc(get_offset_pc());print_source(line_entry->file->path, line_entry->line);
}uint64_t Debugger::get_offset_pc() {return offset_load_address(get_pc());
}

当行号不一样的时候,有可能进入到了一个函数,也有可能进入了本函数的下一行(本行不是函数)。之所以要用 while() 是因为 prace() 只能按照汇编指令一行一行执行,源代码一行可能对应着多行汇编指令。

四、step_over

step_over意味着如果下一行是一个函数,会直接运行下一行结束,而不是进入函数,并且会停在下下一行。

A couple of horrible options are to keep stepping until we’re at a new line in the current function, or to set a breakpoint at every line in the current function. The former would be ridiculously inefficient if we’re stepping over a function call, as we’d need to single step through every single instruction in that call graph, so I’ll go for the second solution.

这里就需要用第二个方法来解决这个问题,即在当前函数中给所有的行(除了本行)打上断点,然后 continue,那么将会停在下一个断点,也就是下下一行。这个方法很妙,当然第一种方法也不赖,它的思路是一直指令级别的 step,然后停在行数变化的那一行,这种方法的缺点是效率低。这里用第二种方法,打断点的方法。

当然了,打完断点还得取消断点,这里得设计一个容器,将断点装起来,然后再取消掉。还有一个细节问题,就是如果直行到函数的最后一行,这时候就需要运行完停留在 main()f() 的下一行了,因此这里还必须将 f() 的返回地址也打一个断点:

void f() {int foo = 1;		int foo1 = 1;e();int foo2 = 1;int foo3 = 1;		<-----执行在此处
}int main() {int foo = 1;int foo1 = 1;int foo2 = 1;int foo3 = 1;f();int foo4 = 1;
}
void Debugger::step_over() {auto func = get_function_from_pc(get_offset_pc());auto func_entry = at_low_pc(func);auto func_end = at_high_pc(func);auto line = get_line_entry_from_pc(func_entry);auto start_line = get_line_entry_from_pc(get_offset_pc());std::vector<std::intptr_t> to_delete{};while (line->address < func_end) {auto load_address = offset_dwarf_address(line->address);if (line->address != start_line->address && !m_breakpoints.count(load_address)) {set_breakpoint_at_address(load_address);to_delete.push_back(load_address);}++line;}// 获取本函数的返回地址防止这是本函数的最后一行auto frame_pointer = get_register_value(m_pid, reg::rbp);auto return_address = read_memory(frame_pointer+8);if (!m_breakpoints.count(return_address)) {set_breakpoint_at_address(return_address);to_delete.push_back(return_address);}continue_execution();for (auto addr : to_delete) {remove_breakpoint(addr);}
}

这篇关于debugger(五):source level stepping的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1052129

相关文章

Debugging Lua Project created in Cocos Code IDE creates “Waiting for debugger to connect” in Win-7

转自 I Installed Cocos Code IDE and created a new Lua Project. When Debugging the Project(F11) the game window pops up and gives me the message waiting for debugger to connect and then freezes. Also a

10 Source-Get-Post-JsonP 网络请求

划重点 使用vue-resource.js库 进行网络请求操作POST : this.$http.post ( … )GET : this.$http.get ( … ) 小鸡炖蘑菇 <!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-w

fetch-event-source 如何通过script全局引入

fetchEventSource源码中导出了两种类型的包cjs和esm。但是有个需求如何在原生是js中通过script标签引呢?需要加上type=module。今天介绍另一种方法 下载源码文件: https://github.com/Azure/fetch-event-source.git 安装: npm install --save-dev webpack webpack-cli ts

MiniCPM-V: A GPT-4V Level MLLM on Your Phone

MiniCPM-V: A GPT-4V Level MLLM on Your Phone 研究背景和动机 现有的MLLM通常需要大量的参数和计算资源,限制了其在实际应用中的范围。大部分MLLM需要部署在高性能云服务器上,这种高成本和高能耗的特点,阻碍了其在移动设备、离线和隐私保护场景中的应用。 文章主要贡献: 提出了MiniCPM-V系列模型,能在移动端设备上部署的MLLM。 性能优越:

Open Source, Open Life 第九届中国开源年会论坛征集正式启动

中国开源年会 COSCon 是业界最具影响力的开源盛会之一,由开源社在2015年首次发起,而今年我们将迎来第九届 COSCon! 以其独特定位及日益增加的影响力,COSCon 吸引了越来越多的国内外企业、高校、开源组织/社区的大力支持。与一般企业、IT 媒体、行业协会举办的行业大会不同,COSCon 具有跨组织、跨项目、跨社区的广泛覆盖面,也吸引了众多国内外开源开发者和开源爱好者的关注及参与

JS实现将两个相同的json对象合并成为一个新对象(对象中包含list或者其他对象)source===target(不破坏target的非空值)

重点申明一下, 这个方法 只限于两个完全一样的对象 ,不一样的对象请使用 下面的进行合并,   <script>let form = {name: 'liming', sex: '男'};let obj = {class: '一班', age: 15};console.log('before', form);Object.assign(form, obj); //该方法可以完成console.

PAT (Advanced Level) Practice——1011,1012

1011:  链接: 1011 World Cup Betting - PAT (Advanced Level) Practice (pintia.cn) 题意及解题思路: 简单来说就是给你3行数字,每一行都是按照W,T,L的顺序给出相应的赔率。我们需要找到每一行的W,T,L当中最大的一个数,累乘的结果再乘以0.65,按照例子写出表达式即可。 同时还需要记录每一次选择的是W,T还是L

Matlab_learning_2(Pie‘s source code饼状图源码)

一、源代码 function hh = pie(varargin)%PIE Pie chart.% PIE(X) draws a pie plot of the data in the vector X. The values in X% are normalized via X/SUM(X) to determine the area of each slice of p

The steps for download android source code

The steps for download android source code. Except for the git tool, all the other steps is for both Windows and Linux. 以下描述是Windows上的操作步骤,其实windows和Linux上面的执行过程没有多大差别,仅在于git安装、Python脚本改成和机器上Pytho

N-ary Tree Level Order Traversal

Input: root = [1,null,3,2,4,null,5,6]Output: [[1],[3,2,4],[5,6]] 思路:就是一个queue的level order 收集; /*// Definition for a Node.class Node {public int val;public List<Node> children;public Node() {}pu