1. 程式人生 > >gdb如何調用被調試任務的特定函數

gdb如何調用被調試任務的特定函數

intel 取代 復雜 spa mac 跳轉 恢復 port org

一、gdb中函數調用
在gdb中,可以通過
call function(args……)
來調用一個函數,當然也可以使用print之類的函數來間接的調用一個函數,但是不管如何,它們最終都要求gdb來調用一個函數,執行該函數,取函數返回值等基本邏輯處理。現在想一下gdb是如何讓被調試進程執行特定函數的,這裏包含了參數的傳遞,返回值的提取,並且最為重要的是要保證只執行這個函數(也就是函數返回之後它如何收場)。特別是最後一個,函數如何結束的問題是一個比較棘手的問題,因為一個函數的返回點可能並不唯一,gdb沒有辦法在函數的最後加一個斷點來等待執行完成,如果gdb不能在函數執行結束之後及時斷住被調試任務,可能造成不可預知的災難性後果。

二、gdb實現
gdb中關於這個函數的實現位於
gdb-6.0\gdb\infcall.c
文件中的
call_function_by_hand (struct value *function, int nargs, struct value **args)
函數,具體函數細節我們就不詳細分析了(當然不是因為我不懂),這裏只是大致說一下gdb是如何解決上面說的問題的。
1、堆棧及參數如何確定
對於函數來說,它首先要有一個堆棧,這個棧不僅包含了它自己使用的函數內堆棧,還要有自己的參數以及返回地址等信息,這些一般是由調用者代勞完成的,這是函數調用的規則。現在是gdb人工培育的一個函數調用,所以這個工作就理所當然的要由gdb來完成這個參數列表以及返回地址等函數使用的上下文的設置。這一點並不復雜,因為每種體系結構有自己的函數調用約定,例如,對於386來說,在進入函數之前,esp指向返回地址,esp+4為第一個參數,esp+8為第二個參數一次類推,當然有些比較特殊的調用需要在編譯時聲明,例如一些使用寄存器傳遞參數的函數調用,這些都會在可執行文件對應的調試信息中有保存,所以也不是什麽難題,就像協議一樣,照著協議的內容來做就不會有問題,因為關鍵就是一致嘛。
那麽這個堆棧在哪裏開呢?最為直觀和簡單的就是在當前調試的任務(線程)使用的堆棧棧頂之上開棧,這一點您也別大驚小怪,因為在Linux下的信號處理函數就是這麽搞的,它就是使用了被中斷線程的堆棧頂端的堆棧,所以這裏gdb在被調試任務的棧頂建立自己的堆棧也是有情可原的,而且是最為直接的一種實現,這裏不是高潮。
2、如何優雅的執行並只執行這一個函數
難點依然在於第一節中說的那個,函數執行完之後如何收場?在上面提到過,gdb要為函數設置參數和返回地址,不論函數內部有幾個返回點,它返回之後總是會跳轉到調用者給它提供的一個地址上來,所以我們可以在這個返回值上做文章,讓它返回到一個特定的地址,然後我們在這個地址上打斷點,從而可以知道該函數執行結束。使用gdb的斷點功能,斷點要設置到代碼段中,即使設置到其它地方,我們如何保證這個斷點不是其它線程正常路徑下的一個函數調用而是我們手動調用函數的實現呢?所以這個返回地址的選擇就非常重要,大家可以自己想一下設置到哪裏。當然我是看了代碼知道它,它是設置在了整個用戶態程序的入口處,也就是從內核態到達用戶態之後執行的第一條指令的地址,或者說這個程序的入口位置,這個位置在程序運行起來之後只會被被運行一次,而不會被其它任何的任務再調動,因為它是最為原始的入口,如果有人執行到這裏,那麽整個程序都要重新執行一次,所以這個是最為合適的一個位置。那麽gdb如何知道這個位置呢?在elf文件中,在ELF文件的頭部保存了這個文件的入口,我們隨便找一個可執行文件,看一下它的入口
[tsecer@Harry localstatic]$ readelf -h /bin/cat
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2‘s complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x80490f0
這裏就是這個入口地址,gdb源代碼中對於該地址的讀取代碼為
#define CALL_DUMMY_ADDRESS() (entry_point_address ())
/* Get current entry point address. */

CORE_ADDR
entry_point_address (void)
{
return symfile_objfile ? symfile_objfile->ei.entry_point : 0;
}
三、驗證gdb實現
[tsecer@Harry gdbentry]$ cat gdbentry.c
#include <stdlib.h>
#include <stdio.h>
void dumpmem(int * start, int len)/*顯示從start開始的len個整數*/
{
int i ;
for(i =0 ; i < len ; i++)
{
if(!(i&0x3))
printf("\n%#-08x:\t",start+i);
printf("%08x\t",start[i]);
}
printf("\n");
}
int handy(int * addr,int naptime)
{
int* myesp;
__asm__ __volatile__ (
"movl %%esp,%0\n"
:"=&r"(myesp)
);/*該匯編指令將esp地址放入myesp中,供下面打印堆棧內容*/
printf("Dumping start from esp:\n");/*這裏顯示gdb調動的堆棧內容*/
dumpmem(myesp,16);
printf("Dumping entry from %#x is %#x end\n",addr,addr[0]);/*顯示整個可執行文件的入口處內容,修改為斷點指令0xcc*/
dumpmem(addr,1);
printf("enddumping");
sleep(naptime);
}
int main()
{
sleep(1000000);
}
[tsecer@Harry gdbentry]$ gcc -g -o gdbentry.c.exe gdbentry.c
[tsecer@Harry gdbentry]$ readelf -h gdbentry.c.exe
ELF Header:
Magic: 7f 45 4c 46 01 01 01 03 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2‘s complement, little endian
Version: 1 (current)
OS/ABI: UNIX - Linux
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x80483a0 整個可執行文件的入口地址,這將會作為手動調用函數的返回地址
Start of program headers: 52 (bytes into file)
Start of section headers: 3780 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 8
Size of section headers: 40 (bytes)
Number of section headers: 38
Section header string table index: 35
[tsecer@Harry gdbentry]$ gdb gdbentry.c.exe
GNU gdb (GDB) Fedora (7.0-3.fc12)
Copyright (C) 2009 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/tsecer/CodeTest/gdbentry/gdbentry.c.exe...done.
(gdb) b main
Breakpoint 1 at 0x804853c: file gdbentry.c, line 30.
(gdb) r
Starting program: /home/tsecer/CodeTest/gdbentry/gdbentry.c.exe

Breakpoint 1, main () at gdbentry.c:30
30 sleep(1000000);
Missing separate debuginfos, use: debuginfo-install glibc-2.11.2-3.i686
(gdb) call hand
handle_amd handle_intel handy
(gdb) call handy(0x80483a0,2) 手動調用handy函數,該函數會顯示函數入口處內容,可以看到,其中入口處已經被修改為intel的調試中斷指令0xcc(int 3)
Dumping start from esp:

0xbffff2b8: bffff2b8 00000010 bffff2e8 080497ec
0xbffff2c8: bffff2d8 08048320 bffff318 bffff2b8
0xbffff2d8: bffff308 08048579 bffff2e4 080483a0
0xbffff2e8: 080483a0 00000002 08048560 080483a0
Dumping entry from 0x80483a0 is 0x895eedcc end

0x80483a0: 895eedcc
$1 = 0
(gdb) x/x 0x80483a0
0x80483a0 <_start>: 0x895eed31函數執行結束之後,這裏可以看到此處恢復為代碼段的原始值
(gdb)
四、為什麽不是用/proc/pid/mem來看而是由handy來顯示?
因為內核中限制了對進程mem文件的讀取非常苛刻,只有進程自己或者它的調試進程可以讀取,普通進程即使是root也不能讀取其它進程的mem文件
linux-2.6.21\fs\proc\base.c
static ssize_t mem_read(struct file * file, char __user * buf,
size_t count, loff_t *ppos)
……
if (!MAY_PTRACE(task) || !ptrace_may_attach(task))
goto out;
#define MAY_PTRACE(task) \
(task == current || \
(task->parent == current && \
(task->ptrace & PT_PTRACED) && \
(task->state == TASK_STOPPED || task->state == TASK_TRACED) && \
security_ptrace(current,task) == 0))

gdb如何調用被調試任務的特定函數