GDB 單步調試匯編


本文同時發表在 https://github.com/zhangyachen/zhangyachen.github.io/issues/134

之前在看匯編的時候一直是肉眼看GCC -S的結果,缺點是很不直觀,無法實時的看到寄存器的值,所以研究了下如何用GDB調試匯編。當然,寫這篇文章更重要的一個目的是半年沒有寫博客了,博客要長草了。^_^

我調試匯編的需求有幾點:

  • 能夠單步進行匯編調試。
  • 能夠實時看到寄存器值的變化。
  • 能夠看到源代碼和對應匯編的關系。

下面分享下用GDB實現上面的3點需求:

單步進行匯編調試

使用si和ni。與s與n的區別在於:s與n是C語言級別的單步調試,si與ni是匯編級別的單步調試。

能夠實時看到寄存器值的變化。

使用gdb時增加-tui選項,打開gdb后運行layout regs命令。注意最好加上-tui,否則很大可能會出現花屏現象。

image

能夠看到源代碼和對應匯編的關系

在gdb中運行set disassemble-next-line on,表示自動反匯編后面要執行的代碼。

image

可以清晰的看出int c=sum(x,y);與下面紅框內的匯編指令成對應關系。

如果大家不想用這么原始的方式,可以給GDB安裝插件或者使用emacs達到上面的目的,推薦兩篇文章:

最后以一個小例子結束:

int sum(int x,int y){
        return x+y;
}

int main(){
        int x=10;
        int y=20;
        int c=sum(x,y);

        return 0;
}

gcc版本4.4.7,默認的優化選項。

我們單步調試下這段代碼對應的匯編:

設置斷點

注意如果想要把斷點設置在匯編指令層次函數的開頭,應該使用b *fun而不是b func,這里我們把斷點設置在b *main

分配棧幀

0x0000000000400489 <main+0>:	 55	push   %rbp
0x000000000040048a <main+1>:	 48 89 e5	mov    %rsp,%rbp
0x000000000040048d <main+4>:	 48 83 ec 10	sub    $0x10,%rsp

%rbp和%rsp表示的是當前棧幀的棧底和棧頂。其中%rbp是被調用者需要保存的寄存器。sub $0x10,%rsp表示為main函數分配棧幀空間。
注意這里分配了16字節的棧空間,會有4字節用不上,我個人猜測跟gcc匯編產生的cfi_def_cfa_offset 16有關,這個沒有深究。

int x=10

0x0000000000400491 <main+8>:	 c7 45 f4 0a 00	00 00	movl   $0xa,-0xc(%rbp)

將x的值放到棧中

int y=20

0x0000000000400498 <main+15>:         c7 45 f8 14 00	00 00	movl   $0x14,-0x8(%rbp)

將y的值放到棧中

sum函數調用

 0x000000000040049f <main+22>:         8b 55 f8	mov    -0x8(%rbp),%edx
 0x00000000004004a2 <main+25>:         8b 45 f4	mov    -0xc(%rbp),%eax
 0x00000000004004a5 <main+28>:         89 d6	mov    %edx,%esi
 0x00000000004004a7 <main+30>:         89 c7	mov    %eax,%edi
 0x00000000004004a9 <main+32>:         e8 c6 ff ff ff	callq  0x400474	<sum>

將x與y分別賦值到%esi和%edi中,其中%edi和%esi被規定用來傳遞函數的第一個和第二個參數。(一個疑問是為什么不能直接mov -0x8(%rbp),%esi呢?)
callq會將下一條指令的地址壓入棧中,並跳到sum函數的第一條指令。

進入sum函數

0x0000000000400474 <sum+0>:	 55	push   %rbp
0x0000000000400475 <sum+1>:	 48 89 e5	mov    %rsp,%rbp
0x0000000000400478 <sum+4>:	 89 7d fc	mov    %edi,-0x4(%rbp)
0x000000000040047b <sum+7>:	 89 75 f8	mov    %esi,-0x8(%rbp)

同main函數一樣,首先將%rbp保存,然后從%edi和%esi中取出函數參數。

求和

0x000000000040047e <sum+10>:	 8b 45 f8	mov    -0x8(%rbp),%eax
0x0000000000400481 <sum+13>:	 8b 55 fc	mov    -0x4(%rbp),%edx
0x0000000000400484 <sum+16>:	 8d 04 02	lea    (%rdx,%rax,1),%eax

將x和y相加,這里用到的是lea指令,關於lea指令介紹參考LEA instruction? ,這里不贅述了。
將返回值放到%eax中,%rax寄存器規定存放函數的返回值。像GO語言如果函數可以有多個返回值的話,返回值是放到棧中。

sum函數收尾

0x0000000000400487 <sum+19>:	 c9	leaveq
0x0000000000400488 <sum+20>:	 c3	retq

我們先看下現在的棧:

image
(這里不知道為什么沒有sub xx,$rsp,我猜測是gcc發現這個最后一次函數調用,之后不會有棧的增長只會有棧的回退,所以用%rsp和%rbp的結果是一樣的。簡單驗證了下,應該是這樣)。
在函數結束時首先需要回收當前函數的棧幀、恢復保存過的寄存器、恢復%rip的值,即返回地址。

leaveq指令相當於:

mov  %rbp,%rsp     
pop %rbp

作用是釋放(deallocate)當前函數的棧幀並恢復被保存的寄存器的值。由此我們也可以看出%rbp的作用:記住%rsp應該回退的位置,否則函數結束時%rsp不知道該回退到哪。

req指令相當於:

pop %rip

將上面保存過的callq的下一條指令地址恢復到%rip中。

接收函數返回值

0x00000000004004ae <main+37>:         89 45 fc	mov    %eax,-0x4(%rbp)

將%eax的值放入到main函數的棧幀中。

return 0

0x00000000004004b1 <main+40>:         b8 00 00 00 00	mov    $0x0,%eax

同上面sum函數一樣。

main函數收尾

0x00000000004004b6 <main+45>:         c9	leaveq
0x00000000004004b7 <main+46>:         c3	retq

如果上面%rsp和%rbp指向同一內存區域看起來不太直觀的話,看下現在main函數即將結束時的棧空間:

image

同上面sum函數的解釋一樣,不再贅述。

程序運行成功退出。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM