gdb调用函数

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_43173244/article/details/87251580

在这周,我发现我可以从 gdb 上调用 C 函数。这看起来很酷,因为在过去我认为 gdb 最多只是一个只读调试工具。

我对 gdb 能够调用函数感到很吃惊。正如往常所做的那样,我在 Twitter 上询问这是如何工作的。我得到了大量的有用答案。我最喜欢的答案是 Evan Klitzke 的示例 C 代码,它展示了 gdb 如何调用函数。代码能够运行,这很令人激动!

我(通过一些跟踪和实验)认为那个示例 C 代码和 gdb 实际上如何调用函数不同。因此,在这篇文章中,我将会阐述 gdb 是如何调用函数的,以及我是如何知道的。

关于 gdb 如何调用函数,还有许多我不知道的事情,并且,在这儿我写的内容有可能是错误的。

从 gdb 中调用 C 函数意味着什么?

在开始讲解这是如何工作之前,我先快速的谈论一下我是如何发现这件令人惊讶的事情的。

假如,你已经在运行一个 C 程序(目标程序)。你可以运行程序中的一个函数,只需要像下面这样做:

  • 暂停程序(因为它已经在运行中)
  • 找到你想调用的函数的地址(使用符号表)
  • 使程序(目标程序)跳转到那个地址
  • 当函数返回时,恢复之前的指令指针和寄存器

通过符号表来找到想要调用的函数的地址非常容易。下面是一段非常简单但能够工作的代码,我在 Linux 上使用这段代码作为例子来讲解如何找到地址。这段代码使用 elf crate。如果我想找到 PID 为 2345 的进程中的 foo 函数的地址,那么我可以运行 elf_symbol_value("/proc/2345/exe", "foo")

 

1

		<p>2</p>

		<p>3</p>

		<p>4</p>

		<p>5</p>

		<p>6</p>

		<p>7</p>

		<p>8</p>

		<p>9</p>

		<p>10</p>

		<p>11</p>

		<p>12</p>

		<p>13</p>

		<p>14</p>
		</td>
		<td>
		<p>fn elf_symbol_value(file_name: &amp;str, symbol_name: &amp;str) -&gt; Result&lt;u64, Box&lt;std::error::Error&gt;&gt; {</p>

		<p>&nbsp;&nbsp;&nbsp;&nbsp;// 打开 ELF 文件</p>

		<p>&nbsp;&nbsp;&nbsp;&nbsp;let file = elf::File::open_path(file_name).ok().ok_or("parse error")?;</p>

		<p>&nbsp;&nbsp;&nbsp;&nbsp;// 在所有的段 &amp; 符号中循环,直到找到正确的那个</p>

		<p>&nbsp;&nbsp;&nbsp;&nbsp;let sections = &amp;file.sections;</p>

		<p>&nbsp;&nbsp;&nbsp;&nbsp;for s in sections {</p>

		<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for sym in file.get_symbols(&amp;s).ok().ok_or("parse error")? {</p>

		<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if sym.name == symbol_name {</p>

		<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return Ok(sym.value);</p>

		<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}</p>

		<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}</p>

		<p>&nbsp;&nbsp;&nbsp;&nbsp;}</p>

		<p>&nbsp;&nbsp;&nbsp;&nbsp;None.ok_or("No symbol found")?</p>

		<p>}</p>
		</td>
	</tr></tbody></table></div><p>这并不能够真的发挥作用,你还需要找到文件的内存映射,并将符号偏移量加到文件映射的起始位置。找到内存映射并不困难,它位于&nbsp;<code>/proc/PID/maps</code>&nbsp;中。</p>

总之,找到想要调用的函数地址对我来说很直接,但是其余部分(改变指令指针,恢复寄存器等)看起来就不这么明显了。

你不能仅仅进行跳转

我已经说过,你不能够仅仅找到你想要运行的那个函数地址,然后跳转到那儿。我在 gdb 中尝试过那样做(jump foo),然后程序出现了段错误。毫无意义。

如何从 gdb 中调用 C 函数

首先,这是可能的。我写了一个非常简洁的 C 程序,它所做的事只有 sleep 1000 秒,把这个文件命名为 test.c :

 

1

		<p>2</p>

		<p>3</p>

		<p>4</p>

		<p>5</p>

		<p>6</p>

		<p>7</p>
		</td>
		<td>
		<p>#include &lt;unistd.h&gt;</p>

		<p>int foo() {</p>

		<p>&nbsp;&nbsp;&nbsp;&nbsp;return 3;</p>

		<p>}</p>

		<p>int main() {</p>

		<p>&nbsp;&nbsp;&nbsp;&nbsp;sleep(1000);</p>

		<p>}</p>
		</td>
	</tr></tbody></table></div><p>接下来,编译并运行它:</p>

 

1

		<p>2</p>
		</td>
		<td>
		<p>$ gcc -o test&nbsp;&nbsp;test.c</p>

		<p>$ ./test</p>
		</td>
	</tr></tbody></table></div><p>最后,我们使用 gdb 来跟踪&nbsp;<code>test</code>&nbsp;这一程序:</p>

 

1

		<p>2</p>

		<p>3</p>

		<p>4</p>
		</td>
		<td>
		<p>$ sudo gdb -p $(pgrep -f test)</p>

		<p>(gdb) p foo()</p>

		<p>$1 = 3</p>

		<p>(gdb) quit</p>
		</td>
	</tr></tbody></table></div><p>我运行&nbsp;<code>p foo()</code>&nbsp;然后它运行了这个函数!这非常有趣。</p>

这有什么用?

下面是一些可能的用途:

  • 它使得你可以把 gdb 当成一个 C 应答式程序(REPL),这很有趣,我想对开发也会有用
  • 在 gdb 中进行调试的时候展示/浏览复杂数据结构的功能函数(感谢 @invalidop
  • 在进程运行时设置一个任意的名字空间(我的同事 nelhage 对此非常惊讶)
  • 可能还有许多我所不知道的用途

它是如何工作的

当我在 Twitter 上询问从 gdb 中调用函数是如何工作的时,我得到了大量有用的回答。许多答案是“你从符号表中得到了函数的地址”,但这并不是完整的答案。

有个人告诉了我两篇关于 gdb 如何工作的系列文章:原生调试:第一部分原生调试:第二部分。第一部分讲述了 gdb 是如何调用函数的(指出了 gdb 实际上完成这件事并不简单,但是我将会尽力)。

步骤列举如下:

  1. 停止进程
  2. 创建一个新的栈框(远离真实栈)
  3. 保存所有寄存器
  4. 设置你想要调用的函数的寄存器参数
  5. 设置栈指针指向新的栈框stack frame
  6. 在内存中某个位置放置一条陷阱指令
  7. 为陷阱指令设置返回地址
  8. 设置指令寄存器的值为你想要调用的函数地址
  9. 再次运行进程!

(LCTT 译注:如果将这个调用的函数看成一个单独的线程,gdb 实际上所做的事情就是一个简单的线程上下文切换)

我不知道 gdb 是如何完成这些所有事情的,但是今天晚上,我学到了这些所有事情中的其中几件。

创建一个栈框

如果你想要运行一个 C 函数,那么你需要一个栈来存储变量。你肯定不想继续使用当前的栈。准确来说,在 gdb 调用函数之前(通过设置函数指针并跳转),它需要设置栈指针到某个地方。

这儿是 Twitter 上一些关于它如何工作的猜测:

我认为它在当前栈的栈顶上构造了一个新的栈框来进行调用!

以及

你确定是这样吗?它应该是分配一个伪栈,然后临时将 sp (栈指针寄存器)的值改为那个栈的地址。你可以试一试,你可以在那儿设置一个断点,然后看一看栈指针寄存器的值,它是否和当前程序寄存器的值相近?

我通过 gdb 做了一个试验:

 

1

		<p>2</p>

		<p>3</p>

		<p>4</p>

		<p>5</p>

		<p>6</p>

		<p>7</p>

		<p>8</p>
		</td>
		<td>
		<p>(gdb) p $rsp</p>

		<p>$7 = (void *) 0x7ffea3d0bca8</p>

		<p>(gdb) break foo</p>

		<p>Breakpoint 1 at 0x40052a</p>

		<p>(gdb) p foo()</p>

		<p>Breakpoint 1, 0x000000000040052a in foo ()</p>

		<p>(gdb) p $rsp</p>

		<p>$8 = (void *) 0x7ffea3d0bc00</p>
		</td>
	</tr></tbody></table></div><p>这看起来符合“gdb 在当前栈的栈顶构造了一个新的栈框”这一理论。因为栈指针(<code>$rsp</code>)从&nbsp;<code>0x7ffea3d0bca8</code>&nbsp;变成了&nbsp;<code>0x7ffea3d0bc00</code>&nbsp;—— 栈指针从高地址往低地址长。所以&nbsp;<code>0x7ffea3d0bca8</code>&nbsp;在&nbsp;<code>0x7ffea3d0bc00</code>&nbsp;的后面。真是有趣!</p>

所以,看起来 gdb 只是在当前栈所在位置创建了一个新的栈框。这令我很惊讶!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值