零.閱讀目的
C++開發的遊戲服務器避免不了偶爾出現的宕機問題,在查找宕機問題時,一般都會分析dump,但由於編譯器優化問題和64位dump調試的不方便,能看懂彙編可以起到事半功倍的效果,通常可以通過反彙編查找空指針或者程序的執行過程,所以閱讀本書的目標是:看懂彙編,並不深究。
一.彙編基礎
1.基礎指令彙總
- mov //傳送指令
- cmov //條件傳送指令
- xchg //交換指令
- push //壓棧
- pop //出棧
- pusha/popa //壓入/彈出所有16位通用寄存器
- pushad/popad //壓入/彈出所有32位通用寄存器
- add //加法
- sub //減法
- inc //自增
- dec //遞減
- mul //無符號乘
- imul //帶符號乘
- div //無符號除
- idiv //帶符號除
- sal //向左移位,右邊補0
- shr //無符號,向右移位,左邊補0
- sar //帶符號,向右移位,左邊補1
- lea //賦值地址
- xor //異或,可用來清零 ^
- or //或者 ||
- not //非 !
- and //並且 &&
- nop //空指令
- test //位判斷
- je //j開頭的均爲條件跳轉指令,帶n的爲反義
- call //調用函數
- enter //替代函數操作esp,pushl %ebp movl %esp, %ebp
- leave //替代函數操作esp,movl %ebp, %esp popl %ebp
- ret //函數返回指令
- jmp //跳轉到某個地址
- int //中斷
- rep //重複執行某個操作,知道ecx爲0
- loop //循環直到ecx寄存器爲0
以上列舉了一些非常常見的彙編指令,在調試過程中,這些指令無處不在,也是必須要掌握的基本指令。
2.數據類型
- AT&T語法
使用L,W,B來表示數據大小,分別代表四位long,兩位word,一位byte; - intel語法
byte(字節)、word(字)、dword(雙字)、qword(四字)、tbyte(十字節),可以放在ptr前面
二.通用寄存器
1.32位寄存器
- EAX 用於操作數和結果數據的累加器
- EBX 指向數據內存段中的數據的指針
- ECX 字符串和循環操作的計數器
- EDX I/O指針
- EDI 用於字符串操作的目標的數據指針
- ESI 用於字符串操作的源的數據指針
- ESP 堆棧指針
- EBP 堆棧數據指針
2.64位寄存器
- RAX
- RBX
- RCX
- RDX
- RDI
- RSI
- RSP
- RBP
3.系統寄存器
- EIP 系統寄存器,用來記錄CPU要執行的指令地址
4.寄存器的特定使用
linux程序中,程序的退出的狀態碼保存在%ebx寄存器中
movl $8, %ebx
echo $? //可以顯示上一個程序的退出碼,也就是ebx寄存器的值
linux平臺可以使用echo $?查看程序返回值,若使用匯編變成,那麼可以將返回值傳送到ebx寄存器
5.8位、16位、32位寄存器
位數 | 寄存器 | 寄存器 | 寄存器 | 寄存器 |
---|---|---|---|---|
32位 | EAX | EBX | ECX | EDX |
16位 | AX | BX | CX | DX |
8位 | AH/AL | BH/BL | CH/CL | DH/DL |
三.開發工具
1.彙編器
MASM 微軟開發的 http://www.masm32.com/
NASM
GAS GNU系列,另外有gcc、g++
HLA
2.連接器
ld:把彙編語言目標代碼和其他庫連接在一起,生成操作系統可執行文件
3.調試器
gdb:停止程序、檢查修改數據
4.編譯器
as:把高級語言轉換爲處理器能夠執行的指令碼
5.目標代碼反彙編器
objdump:將可執行文件或者目標代碼文件轉換成彙編語言
6.簡檔器
gprof:跟蹤每個函數在程序執行過程中被使用時花費了多長處理器時間
7.一些需要用到的工具
gdb
kdbg 圖形化調試工具
objdump 查看反彙編
gprof 性能分析工具:可以查看函數被調用多少次多少時間
gcc -o demo demo.c -pg
./demo
gprof demo > gprof.txt
gcc過程:
gcc -S ctest.c //生成ctest.s
as ctest.s -o ctest.o
ld ctest.o -o ctest //這一步如果涉及到調用C庫函數,那麼就得帶其他參數
四.操作碼語法(Intel和AT&T的語法不同)
AT&T和intel彙編語法,比較明顯的是操作數順序相反,主要區別有以下幾點:
編號 | Intel | AT&T | AT&T說明 |
---|---|---|---|
1 | 4 | $4 | AT&T使用$表示立即操作數 |
2 | eax | %eax | AT&T在寄存器名稱前面加上前綴% |
3 | mov eax, 4 | movl $4, %eax | 處理源和目標使用相反的順序 |
4 | mov eax, dword ptr test | movl $test, %eax | AT&T不用指定數據長度,但mov後面要指定L,W,B |
5 | jmp section:offset | ljmp |
長調用和跳轉使用不同語法定義段和偏移值 |
6 | -4(%ebp) | [ebp-4] | 間接尋址 |
7 | foo(,%eax,4) | [foo + eax*4] | 間接尋址 |
這裏只是列舉了幾個常見並且比較基本的區別,太複雜的語法沒有深究。
五.彙編程序
1.基本模板
#註釋
.section .data
.section .bss
.section .text
.globl _start
_start:
movl $0, %eax
2.編譯
as cpuid.s -o cpuid.o (-gstabs 添加調試信息)
ld cpuid.o -o cpuid
3.調試(幾個gdb常用調試命令)
- break * label + offset 下斷點,指定行數也行
- next 下一行
- step 下一步,若有函數,則進入函數
- continue
- run
- info registers 查看寄存器
- print /x $ebx 查看寄存器十六進制值
- layout asm 切到反彙編
- x/nyz: 顯示內存位置
- n是字段數: 個數
- y是輸出格式 c字符 d十進制 x十六進制
- z是顯示字段長度 b字節 h半字 w32位字
- 例子:x/42cb &output 查看該變量42位字符
4.測試程序
#cpuid2.s
.section .data
output:
.asciz "The processor Vendor ID is '%s'\n"
.section .bss
.lcomm buffer, 12
.section .text
.globl _start
_start:
movl $0, %eax
cpuid
movl $buffer, %edi
movl %ebx, (%edi)
movl %edx, 4(%edi)
movl %ecx, 8(%edi)
pushl $buffer
pushl $output
call printf
addl $8, %esp
pushl $0
call exit
編譯運行
gzshun@gzshun-vm:~/c$ as cpuid2.s -o cpuid2.o
gzshun@gzshun-vm:~/c$ ld cpuid2.o -o cpuid2 -lc --dynamic-linker /lib/ld-linux.so.2
gzshun@gzshun-vm:~/c$ ./cpuid2
The processor Vendor ID is 'GenuineIntel'
一開始我閱讀本書的時候,對彙編語法並不是很熟,其中犯了一個低級錯誤,代碼看了很久卻始終找不出問題,有2個地方:
1.%ebx 錯寫成 $ebx
在AT&T語法中,寄存器前面要使用百分號%,而我寫成$號不能解析。
gzshun@gzshun-vm:~/c$ ld cpuid2.o -o cpuid2 -lc --dynamic-linker /lib/ld-linux.so.2
cpuid2.o: In function `_start':
(.text+0xe): undefined reference to `ebx'
2.(%edi) 錯寫成 %edi
一開始我懷疑是作者寫的程序適用於比較古老的操作系統,然後測試了centos、ubuntu和redhat幾個系統都不行,但是運行結果都是得到這樣的結果:segmentation fault (core dumped),後來我就沒管了。在讀完本書後,我再回頭看這個程序的出錯原因,通過gdb調試,才知道原來是%edi沒帶括號,導致movl時把edi指針給破壞掉,其實作者的本意是修改edi指針指向的數據。
gzshun@gzshun-vm:~/c$ as cpuid2.s -o cpuid2.o -gstabs
gzshun@gzshun-vm:~/c$ ld cpuid2.o -o cpuid2 -lc --dynamic-linker /lib/ld-linux.so.2
gzshun@gzshun-vm:~/c$ gdb cpuid2
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
(gdb) b 13
Breakpoint 1 at 0x80481cc: file cpuid2.s, line 13.
(gdb) r
Starting program: /home/gzshun/c/cpuid2
Breakpoint 1, _start () at cpuid2.s:13
13 movl %ebx, %edi
(gdb) x/12c buffer
0xb7fbd5d4 <buffer>: 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000'
0xb7fbd5dc <buffer+8>: 0 '\000' 0 '\000' 0 '\000' 0 '\000'
(gdb) x/12c $edi
0x80492c8 <buffer>: 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000'
0x80492d0 <buffer+8>: 0 '\000' 0 '\000' 0 '\000' 0 '\000'
(gdb) s
14 movl %edx, 4(%edi)
(gdb) x/12c $edi
0x756e6547: Cannot access memory at address 0x756e6547
(gdb)
從調試的打印信息可以看出,在執行完movl %ebx, %edi後,edi寄存器的值就被修改了。
5.數據類型
彙編語言的數據類型用於聲明程序中的變量,跟C語言的類型差不多。
命令 | 數據類型 |
---|---|
.ascii | 字符串 |
.asciz | 空字符結尾的字符串 |
.byte | 字節值 |
.double | 雙精度浮點數 |
.float | 單精度浮點數 |
.int | 32位整數 |
.long | 同.int |
.octa | 16字節整數 |
.quad | 8字節整數,也就是64位 |
.short | 16位整數 |
.single | 同.float |
6.數組
- 聲明
sizes:
.int 100,150,200,250,300 - 訪問
movl $sizes, %edi
movl $2, %edi
movl values(, %edi, 4), %eax //eax寄存器的值被寫錯200,也就是sizes[2]
7.bss段
命令 | 描述 |
---|---|
.comm | 聲明未初始化的數據的通用內存區域 |
.lcomm | 聲明未初始化的數據的本地通用內存區域,會佔用程序空間 |
彙編程序中,如果在.lcomm標籤定義一個很大的數組,那麼生成的程序的大小就會包括數組的大小。
六.指令集語法
1.傳送數據
傳送數據
movl value, %eax //把value的數據值傳送給eax寄存器
movl %ecx, value //把ecx寄存器數據傳送給value使用變址的內存位置(訪問數組)
base_address (offset_address, index, size)
movl $2, %edi
movl values(, %edi, 4), %eax //訪問values數組,以大小爲4字節,偏移爲2個單位的數據,也就是C語言中的values[2]使用寄存器間接尋址
movl $values, %edi //將values的內存地址傳送到edi寄存器
movl $ebx, (%edi) //將ebx寄存器的值傳送到edi寄存器中包含的內存位置,也就是指針
movl %edx, 4(%edi) //edi指針後面的4個字節
movl %edx, -4(%edi) //edi指針前面的4個字節條件傳送指令
cmovx source, destination
比較結果存在EFLAGS寄存器中,有帶符號、無符號之間的比較:
下表是無符號條件傳送指令:指令對 描述 EFLAGS狀態 CMOVA/CMOVNBE 大於/不小於或者等於 (CF或ZF)=0 CMOVAE/CMOVNB 大於或者等於/不小於 CF=0 CMOVNC 無進位 CF=0 CMOVC 進位 CF=1 CMOVB/CMOVNAE 小於/不大於或者等於 CF=1 CMOVBE/CMOVNA 小於或者等於/不大於 (CF或ZF)=1 CMOVE/CMOVZ 等於/零 ZF=1 CMOVNE/COMVNZ 不等於/不爲零 ZF=0 CMOVP/CMOVPE 奇偶校驗/偶校驗 PF=1 CMOVNP/CMOVPO 非奇偶校驗/奇校驗 PF=0 從上表可以看出,無符號條件傳送指令依靠進位、零和奇偶校驗標誌確定兩個操作數之間的區別。
上面的指令有些是用 / 符號隔開指令對,這兩個指令具有相同的含義。比如,一個值大於另外一個值,也可以說是不小於或者等於另外一個值。這兩個條件是等同的,但是二者具有個字的傳送指令,如 CMOVA 和 CMOVNBE .
如果操作數是帶符號值的,就必須使用不同的條件傳送指令集,如下表所示:指令對 描述 EFLAGS狀態 CMOVGE/CMOVNL 大於或者等於/不小於 (SF異或OF)=0 CMOVL/CMOVNGE 小於/不大於或者等於 (SF異或OF)=1 CMOVLE/CMOVNG 小於或者等於/不大於 ((SF異或OF) 或 ZF)=1 CMOVO 溢出 OF=1 CMOVNO 未溢出 OF=0 CMOVS 帶符號(負) SF=1 CMOVNS 無符號(非負) SF=0
2.交換數據
指令 | 描述 |
---|---|
XCHG | xchg src, dst // src和dst交換 |
BSWAP | 0x12345678,轉換後變成0x78563412 |
XADD | xadd src,dst // dst = src + dst |
CMPXCHG | cmpxchg src, dst //dst與eax比較,若相等則src傳送到dst |
CMPXCHG8B | cmpxchg8b dst //將dst和edx:eax比較,若相等則將ecx:ebx傳送到dst,高位:低位 |
3.堆棧
堆棧的地址是向下增長,esp寄存器存儲棧頂指針,會隨着push壓入新數據而遞減。
指令
- pushl source //壓入堆棧
- pushw source
- popl dest //彈出堆棧
- popw dest
- pusha/popa //壓入/彈出所有16位通用寄存器
- pushad/popad //壓入/彈出所有32位通用寄存器
- pushf/popf //壓入/彈出EFLAGS寄存器的低16位
- pushfd/popfd //壓入/彈出EFLAGS寄存器的全部32位
棧順序(從高地址向低地址增長)
intel處理器x86系列系統進程,棧向下,堆向上。
8051的棧是向高地址增長,INTEL的8031、8032、8048、8051系列使用向高地址增長的堆棧;但同樣是INTEL,在x86系列中全部使用向低地址增長的堆棧。其他公司的CPU中除ARM的結構提供向高地址增長的堆棧選項外,多數都是使用向低地址增長的堆棧。
歷史遺留
在沒有MMU(Memory Management Unit/內存管理單元)的時代,爲了最大的利用內存空間,堆和棧被設計爲從兩端相向生長。那麼哪一個向上,哪一個向下呢?
人們對數據訪問是習慣於向上的,比如你在堆中new一個數組,是習慣於把低元素放到低地址,把高位放到高地址,所以堆向上生長比較符合習慣。而棧則對方向不敏感,一般對棧的操作只有push和pop,無所謂向上向下,所以就把堆放在了低端,把棧放在了高端。MMU出來後就無所謂了,只不過也沒必要改了。
函數參數會涉及到堆棧知識,文章中的函數章節會詳細講解C樣式函數堆棧。
4.控制執行流程
函數調用
- jmp location //跳轉指令
- call address //調用函數
- enter == push %ebp mov %esp,%ebp
- leave == mov %ebp,%esp pop %ebp
跳轉指令
jxx address,跳轉指令非常多,都是j開頭的指令:
- ja/jg // > above greater
- jae/jge // >=
- jb/jl // < below less
- jbe/jle // <=
- je //==
- jz //==0 zero
- js //帶符號 sign
- jo //溢出 over
- jp //奇偶校驗
- jcxz //cx寄存器爲0就跳轉
- jecxz //ecx寄存器爲0就跳轉
- jnx //jn開頭的跳轉指令與上面的指令意義相反,這裏就不列出了。
上面指令有ja/jg和jb/jl兩種重複的意義,但是用法卻不同,ja/jb(above/below)用於無符號數值判斷,jg/jl(greater/less)用於帶符號數值判斷。
比較指令
cmp a, b //內部處理:b - a
jge address //此時如果調用jge,在b > a的情況下才會跳轉
循環指令
loop address //循環直到ecx寄存器爲0
若ecx爲0,會導致loop問題,所以使用jcxz/jecxz,在ecx==0的情況下跳轉
5.數字
整數長度
byte word doubleword quadword 與C語言類似
字節順序
注意:內存數據是小端格式(little-endian),寄存器是大端格式(big-endian) ,使用gdb的x/4b &data可以查看字節順序
傳送不同數據大小的數字
假設如果要從16位數字傳送給32位寄存器,那麼要先將高位設置成0:
movl $0, %ebx //這一行也可以用異或代替:xor %ebx
movw %ax, %ebx
intel提供一個命令替代上面的操作:
movzx source, dest //無符號:根據source的位數大小,只拷貝這一部分到dest的低位,其他位設置成0
movsx source, dest //帶符號:傳送帶符號整數,除了拷貝低位,其他位設置成1
MMX整數
movq source, dest //將數據傳送到MMX寄存器中,比如%mm0,%mm1
SSE整數
movdqa source, dest //將數據傳送到XMM寄存器中,比如%xmm0, %xmm1
其他
- 原碼:數據本身
- 反碼:原碼的取反
- 補碼:反碼+1
浮點數
- 科學計數法
0.159 * 10^0 值這樣算:0 + (1/10) + (5/100) + (9/1000) //這是日常人類看得懂的數字 - 二進制浮點數
1.0101 * 2^2 是 101.01,值這樣算:5 + (0/2) + (1/4) = 5.25 //這是計算機看得懂的 例子
二進制 十進制分數 十進制值 0.1 1/2 0.5 0.01 1/4 0.25 0.001 1/8 0.125 二進制浮點格式
浮點類型 符號位 指數 係數(有效數字) float 31 23~30 0~22 double 63 52~62 0~51 float的有效數字有23位,那麼2^23=8388608,結果的長度是7位數字,所以float的精度是7位小數點。
double的有效數字有52位,那麼2^52=4503599627370496,結果的長度是16位,所以double的精度是16位小數點。
浮點數指令
F開頭的指令基本上是浮點數的操作指令,大概瞭解一下就行。
- FLD source //會將浮點數壓入FPU堆棧,st0、st1
- FLDS source //單精度
- FLDL source //雙精度
- FLD1 //加載1
- FLDL2T //log
- FLDL2E //log
- FLDPI //壓入3.1415926
- FLDLG2 //log2
- FLDLN2 //ln
- FLDZ //0.0
SSE浮點
MOVAPS MOVUPS MOVSS MOVLPS MOVHPS MOVLHPS MOVHLPS
這些MMX,SSE高級數據對調試沒什麼幫助,就不深入學習。
6.基本數學功能
- 加法(b、w、l):
add source, dest - 雙字加法(b、w、l):會將進位標誌帶入高位計算
adc source, dest - 減法(b、w、l):
sub source, dest - 雙字減法(b、w、l):無符號的減法,考慮溢出和進位標誌位
sbb source, dest - 遞增/遞減:無符號,不影響進位標誌
inc dest
dec dest 乘法(無符號):
mul source //無符號,目標數隱含,因爲有以下情況源操作數長度 目標操作數 目標位置 8bit AL AX 16bit AX DX:AX 32bit EAX EDX:AX 乘法(帶符號)
imul source //帶符號,帶符號要檢查結果是否溢出,使用jo指令
imul source, dest
imul val, source, dest //dest = val * source除法(無符號)
div divisor //divisor是除數被除數 被除數長度 商 餘數 AX 16bit AL AH DX:AX 32bit AX DX EDX:EAX 64bit EAX EDX 除法(帶符號)
idiv divisor //帶符號移位乘法:右邊補0
sal(向左算術移位)/shl(向左邏輯移位)
sal dest //左移1位
sal %cl, dest //左移寄存器cl中的位數
sal val, dest //左移val位數移位除法
shr dest //無符號,左邊補0
sar dest //帶符號,左邊補1循環移位:移位導致的溢出位放到值的另一端
rol dest //向左移位
ror dest //向右移位
rcl dest //向左移位,包含進位標誌
rcr dest //向右移位,包含進位標誌不打包BCD運算
AAA //在add後面
AAS //在sub後面
AAM //在mul後面
AAD //在div之前打包BCD運算:打包BCD值的是字節低4位放BCD低4位,字節高4位放BCD高4位
DAA //add或adc
DAS //sub或sbb布爾邏輯:
and source, dest
not source, dst
or source, dst
xor source, dst //異或,可用來清零
test source, dst //位測試,比如test $0x10, %eax清空進位標誌:
clc
7.高級數學功能(FPU寄存器)
FPU寄存器寄存器爲R0~R7,用來計算浮點型數據,一些操作指令跟基本的數學操作一樣,只是前面多了一個F。
FPU寄存器堆棧
R0 –> ST7
R1 –> ST6
…
R7 –> ST0常用的浮點計算指令
fadd
fdiv
fdivr
fmul
fsub
fsubr三角函數:
fcos
fsin
fptan
三角函數、對數、平方、絕對值等等
fpu指令暫時沒用到,不再深究。
8.處理字符串
傳送字符串
movs(b,w,l):
隱含的源操作數是esi,隱含的目標操作數是edi,所以完整的指令是這樣:
movs %esi, %edi //但是後面不用寫,省略了,esi表示source,edi表示destination
例子:
movl $input, %esi
movl $output, %edi
movsl地址傳送指令
lea output, %edi //該指令經常用來將內存地址賦值給destDF標誌
每次執行movs指令,esi和edi會改變,若DF標誌位0,則遞增;若DF標誌位1,則遞減
cld //將DF清零
std //設置DF標誌,注意,當向後處理字符串時,movs的指令仍然是向前獲取內存REP前綴(repeat)
代替loop指令,根據ecx的值一直進行處理,直到ecx=0
ecx的長度要根據movsq,movsw,movsl的長度進行改變,若超出字符串邊緣,會導致內存之後的數據也被讀取到
rep //判斷ecx
repe,repne,repnz,repz //判斷ecx和ZF標誌存儲和加載字符串
lods //隱含的操作數是esi寄存器
lodsb //把一個字節加載到AL寄存器中
lodsw //2個字節到AX
lodsl //4個字節到EAX
stos //隱含的操作數是edi寄存器
stosb //AL
stosw //AX
stosl //EAX
lods和stos配合rep前綴,可以複製大型字符串值並處理,可以實現類似memset的功能比較字符串:
cmps(q,w,l)
隱含的參數是esi和edi,也可配合rep使用掃描字符串:
scas(b,w,l)
比較AL,AX,EAX和隱含edi寄存器的操作數,也可配合rep使用
七.函數
1.創建函數
固定格式如下:
.type fun1, @function
area:
ret
ret指令:執行ret,程序返回主程序,返回的位置是緊跟着call指令後面的指令
2.參數和返回結果
- 參數:寄存器、全局變量、堆棧
- 返回結果:寄存器、全局變量
3.調用函數
- 指令
call function - 參數
執行call之前,要把輸入值放在正確的位置。在函數內部,可能會改變寄存器值,爲了確保函數返回後可以恢復寄存器的狀態,可以使用pusha和popa來保證,也可以針對特定的寄存器進行操作。
pusha //同時保存所有寄存器
popa //同時恢復所有寄存器
4.C樣式傳遞數據值(堆棧)
- 參數順序
C樣式函數傳參的解決方案是使用堆棧,參數的堆棧順序與函數原型中的順序相反 返回值
- 使用eax寄存器存儲32位結果
- 使用edx:eax寄存器存儲64位結果
- 使用FPU的ST0存儲浮點值
esp
函數的開頭和結尾,會保存esp,所以函數的格式一般如下:
function:
pushl %ebp
movl %esp, %ebp
...
movl %ebp, %esp
pop %ebp
ret
也可以這樣寫:
function:
enter
...
leave
ret
- 局部變量
在函數開頭,通常會將esp減去一個偏移,這個是開闢了棧空間,函數call結束後,再對esp加了一個偏移,爲了清空堆棧。
堆棧的數據使用%ebp指針進行間接尋址,比如-4(%ebp)是第一個局部變量(等下會討論,這個不一定是第一個局部變量)。
function:
enter
subl $12, %esp
movl $1, -4(%ebp) #這裏就有3個4字節的棧可以用
leave
ret
call fun1
addl $12, %esp #將剛纔開闢的12個字節棧空間清掉
5.函數的堆棧空間
實驗
用一個例子來講解局部變量的棧順序和函數的堆棧空間,在程序開始之前,先給個esp偏移地址對應的參數順序,以下表格是模擬調用一個函數的堆棧空間,從上到下對應高地址到低地址:
地址 | 變量 | ebp偏移 |
---|---|---|
0xbfffefd4 | 函數參數3 | 16(%ebp) |
0xbfffefd0 | 函數參數2 | 12(%ebp) |
0xbfffefcc | 函數參數1 | 8(%ebp) |
0xbfffefc8 | 返回地址 | 4(%ebp) |
0xbfffefc4 | 舊的EBP值 | (%ebp) |
0xbfffefc0 | 局部變量3 | -4(%ebp) |
0xbfffefbc | 局部變量2 | -8(%ebp) |
0xbfffefb8 | 局部變量1 | -12(%ebp) |
我閱讀了這本書的例子,還包括有網上一些堆棧空間的說明,對局部變量的棧順序都有不同的理解,比如上面這個表格,局部變量1到底是對應-4(%ebp),還是對應-12(%ebp)呢?因爲這個對於調試有時是有幫助的,有時想通過esp偏移量得到某個局部變量的大小或者內存,所以瞭解這個順序是很有必要的。
於是使用以下程序進行驗證:
開發環境:Ubuntu 16.04.1 LTS 32位
編譯器:gcc version 5.4.0 20160609
#include <stdio.h>
int fun(int a, int b, int c)
{
int va = a;
int vb = b;
int vc = c;
return va;
}
int main()
{
int result = fun(1, 2, 3);
return 0;
}
使用gcc編譯,用objdump進行反彙編查看對應的main函數和fun函數的彙編代碼:
080483db <fun>:
80483db: 55 push %ebp
80483dc: 89 e5 mov %esp,%ebp
80483de: 83 ec 10 sub $0x10,%esp
80483e1: 8b 45 08 mov 0x8(%ebp),%eax
80483e4: 89 45 f4 mov %eax,-0xc(%ebp)
80483e7: 8b 45 0c mov 0xc(%ebp),%eax
80483ea: 89 45 f8 mov %eax,-0x8(%ebp)
80483ed: 8b 45 10 mov 0x10(%ebp),%eax
80483f0: 89 45 fc mov %eax,-0x4(%ebp)
80483f3: 8b 45 f4 mov -0xc(%ebp),%eax
80483f6: c9 leave
80483f7: c3 ret
080483f8 <main>:
80483f8: 55 push %ebp
80483f9: 89 e5 mov %esp,%ebp
80483fb: 83 ec 10 sub $0x10,%esp
80483fe: 6a 03 push $0x3
8048400: 6a 02 push $0x2
8048402: 6a 01 push $0x1
8048404: e8 d2 ff ff ff call 80483db <fun>
8048409: 83 c4 0c add $0xc,%esp
804840c: 89 45 fc mov %eax,-0x4(%ebp)
804840f: b8 00 00 00 00 mov $0x0,%eax
8048414: c9 leave
8048415: c3 ret
8048416: 66 90 xchg %ax,%ax
8048418: 66 90 xchg %ax,%ax
804841a: 66 90 xchg %ax,%ax
804841c: 66 90 xchg %ax,%ax
804841e: 66 90 xchg %ax,%ax
先看fun函數的彙編代碼,其中有兩行代碼如下:
mov 0x8(%ebp),%eax
mov %eax,-0xc(%ebp)
0x8(%ebp)指向函數參數1,也就是a形參,先把a拷貝到eax寄存器,然後再拷貝到-0xc(%ebp),這時再看C代碼,a形參是賦給va變量的,說明可以得出結論-0xc(%ebp)指向局部變量va,說明越靠後的局部變量的棧地址越靠近ebp,也就是說局部變量的聲明順序與棧空間順序是相反的,得出表格如下:
地址 | 變量 | ebp偏移 |
---|---|---|
0xbfffefd4 | c | 16(%ebp) |
0xbfffefd0 | b | 12(%ebp) |
0xbfffefcc | a | 8(%ebp) |
0xbfffefc8 | 返回地址 | 4(%ebp) |
0xbfffefc4 | 舊的EBP值 | (%ebp) |
0xbfffefc0 | vc | -4(%ebp) |
0xbfffefbc | vb | -8(%ebp) |
0xbfffefb8 | va | -12(%ebp) |
示意圖:
疑問
這裏會有一個疑問,爲什麼vc變量的內存地址是排在ebp的下面(-4(%ebp))?
首先棧空間是從高地址往低地址增長,在函數內部,棧變量隨着靠後的聲明,內存地址會越來越高,當然越高的地址肯定是靠上,也就是接近ebp。特意寫了個程序,把va和vb的內存地址打出來,va的地址小於vb的地址,結合棧空間的結構,可以說明棧順序是這樣:ebp -> vc -> vb -> va。
後來我在csdn發帖詢問,結論是這樣:不同平臺不同編譯器可能不一樣,一般來說,Borland C++、ms VC++按照聲明順序從高地址向低地址排列,intel C++、gcc/g++則相反,按照聲明順序從低地址向高地址排列。
【原貼地址】
6.獨立的函數文件
彙編程序的.globl標籤一般是聲明爲_start,但是獨立函數文件要聲明爲函數名稱:
.section .text
.type area, @function
.globl area
area:
編譯的話跟C語言編譯一樣,各自編譯成.o文件,鏈接的時候使用ld統一將所有.o文件生成可執行程序
有些.s彙編文件要單步調試,有些不用,那就只需要在要調試的彙編文件使用-gstabs進行編譯即可。
7.命令行參數
在Linux系統中,程序運行的虛擬內存地址從0x80480000開始,到地址0xbfffffff結束。所以調試程序時,可以看到地址開頭一般都是8048,因爲程序代碼和數據放在8048那裏,而堆棧數據放在bfffffff那裏,esp指針指向bffffff。
8.系統調用
linux的系統調用函數對應的編號,可以參考unistd.h,裏面內容如下:
#define __NR_exit 1 //exit函數對應的編號不一定是1
調用系統調用
movl $1, %eax //將系統調用編號寫入eax
int 0x80 //使用int進行軟中斷系統調用的參數
eax用於存放函數編號
順序:ebx(第1個參數) -> ecx -> edx -> esi -> edi統計字符串長度
output:
.ascii “helloworld”
output_len:
.equ len, output_len - output複雜的系統調用
比如傳參是一個結構體指針,那麼調用完函數會通過這個指針來獲取數據,可以利用標籤類聲明結構體,每個標籤對應一個變量,內存是連續的跟蹤系統調用
strace //用來跟蹤程序使用了哪些系統調用,返回值,使用時間等等
strace -p pid //動態附加
strace -c 程序 //使用時間
八.內聯彙編
1.使用
在C/C++程序中,使用關鍵之asm,ANSI C使用asm包含的彙編程序
2.語法
asm("movl $1, %eax\n\t"
"movl $0, %ebx\n\t"
"int $0x80")
一定要使用換行符,製表符不是必須的,這種語法只能使用全局變量
編譯後,彙編程序由#APP和#NO_APP包含
asm volatile (“”) //volatile防止編譯器優化
例子:
#include <stdio.h>
int a = 2;
int b = 3;
int result = 0;
int main()
{
asm volatile ("pusha\n\t"
"movl a, %eax\n\t"
"movl b, %ebx\n\t"
"imull %ebx, %eax\n\t"
"movl %eax, result\n\t"
"popa");
printf("The result is %d\n", result);
return 0;
}
3.擴展asm格式
asm(“assembly code” : output location : input operands : changed registers);
asm(彙編程序 : 輸出 : 輸入 : 改變的寄存器)
擴展彙編指定寄存器會用到一張約束表,比如:
- a -> eax ax al //a表示eax寄存器
- b -> ebx
- c -> ecx
- d -> edx
- S -> esi
- D -> edi
- r -> 使用任何可用的通用寄存器,用於佔位符
- m -> 使用變量的內存位置,指令若至少需要一個寄存器,那麼也得使用寄存器配合
其他
輸出修飾符:
+ -> 讀寫
= -> 只寫
%
&
擴展asm的好處是可以使用局部變量,也可以使用佔位符,寄存器使用2個百分號,例子:
int main()
{
int data1 = 10;
int data2 = 20;
int result;
asm("imull %%edx, %%ecx\n\t"
"movl %%ecx, %%eax"
: "=a"(result)
: "d"(data1), "c"(data2));
printf("The result is %d\n", result);
return 0;
}
佔位符例子1:
asm("assembly code"
: "=r"(result)
: "r"(data1), "r"(data2));
彙編語句直接用數字訪問,%0表示result,%1表示data1,%2表示data2
佔位符例子2:
asm("assembly code"
: "=r"(result)
: "r"(data1), "0"(data2));
彙編語句直接用數字訪問,data2前面的0表示和第0個,也就是result公用一個變量
佔位符例子3:
asm("assembly code"
: [value2] "=r"(result)
: [value1] "r"(data1), "0"(data2));
彙編語句使用%[value1]訪問
4.內聯彙編宏函數
跟C語言一樣,可以把asm定義成宏,方便使用。
九.結束
在深入學習調試的過程中,看不懂彙編語言會成爲阻礙前進的絆腳石,於是我就開始閱讀本書,目的很明確,只求看懂不求會寫。所以整個篇幅比較偏向於記錄流水賬,將調試過程中經常會涉及到的點整理下來,對模糊的點編寫程序進行推敲,學完彙編後,今後在調試過程中,可以起到事半功倍的效果。儘管本書是基於Linux GNU編譯器的AT&T彙編語法,但是彙編語法大同小異。
本書爲了講解彙編程序,使用了很多gdb調試技巧,gdb又是另外一塊很大的內容,很值得去學習和整理。
本文只列舉了常用指令,彙編指令非常多,可以到這個頁面查詢。
【彙編指令速查】
作者:gzshun. 原創作品,轉載請標明出處!
來源:http://blog.csdn.net/gzshun