本文同時發表在 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,否則很大可能會出現花屏現象。
能夠看到源代碼和對應匯編的關系
在gdb中運行set disassemble-next-line on
,表示自動反匯編后面要執行的代碼。
可以清晰的看出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
我們先看下現在的棧:
(這里不知道為什么沒有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函數即將結束時的棧空間:
同上面sum函數的解釋一樣,不再贅述。
程序運行成功退出。