【讀書筆記】彙編語言程序設計

這裏寫圖片描述

零.閱讀目的

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 section, offset 長調用和跳轉使用不同語法定義段和偏移值
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 //該指令經常用來將內存地址賦值給dest

  • DF標誌
    每次執行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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章