一、GCC 擴展語法與MSVC約束
(一)GCC(GNU Compiler Collection)內聯匯編語法
asm("匯編指令");#或者
__asm__("匯編指令");#使用更復雜的語法來指定輸入、輸出操作數和修改的寄存器:
asm volatile ("匯編指令1\n\t""匯編指令2\n\t": 輸出操作數列表: 輸入操作數列表: 修改的寄存器列表
);
? 輸出操作數列表:指定匯編指令的輸出結果,格式為[約束符] (C變量),例如"=r" (result)表示將結果存儲到一個通用寄存器中,并賦值給C變量result。
? 輸入操作數列表:指定匯編指令的輸入數據,格式為[約束符] (變量C),例如"r" (input)表示將C變量input的值放入一個通用寄存器作為輸入。
? 修改的寄存器列表:列出匯編代碼中修改的寄存器,防止編譯器對這些寄存器進行優化,例如"cc"表示修改了條件碼寄存器。
示例代碼:
#include <stdio.h>int main() {int a = 10, b = 20, sum;asm volatile ("addl %1, %2\n\t""movl %2, %0\n\t": "=r" (sum) // 輸出操作數: "r" (a), "r" (b) // 輸入操作數: "cc" // 修改的寄存器);printf("Sum = %d\n", sum);return 0;
}
(二)Microsoft Visual C++(MSVC)內聯匯編語法
__asm {匯編指令1;匯編指令2;
}#或者在函數中直接使用asm關鍵字:
void func() {int a = 10, b = 20, sum;asm {mov eax, aadd eax, bmov sum, eax}
}
在MSVC中,內聯匯編代碼使用花括號括起來,匯編指令之間用分號分隔。
二、外部匯編
外部匯編是指將匯編代碼寫在單獨的匯編文件中,然后通過鏈接器將其與C語言代碼鏈接在一起。這種方式適合編寫復雜的匯編代碼模塊。
(一)編寫匯編文件
匯編函數示例:add.asm
section .text
global add
add:mov eax, [esp + 4] ; 獲取第一個參數add eax, [esp + 8] ; 加第二個參數ret
這個匯編函數add接收兩個參數,將它們相加并將結果存儲在eax寄存器中。
(二)編譯和鏈接
1.使用匯編器將匯編文件編譯為目標文件:
nasm -f elf32 add.asm -o add.o
這里使用了NASM匯編器,將add.asm編譯為32位ELF格式的目標文件add.o。
2.將目標文件與C語言代碼編譯鏈接:
gcc main.c add.o -o main
假設main.c是C語言主程序文件,將main.c和add.o鏈接生成可執行文件main。
(三)在C語言中調用匯編函數
在C語言代碼中聲明并調用匯編函數:main.c
extern int add(int, int);int main() {int a = 10, b = 20, sum;sum = add(a, b);printf("Sum = %d\n", sum);return 0;
}
通過extern關鍵字聲明匯編函數add,然后在C語言代碼中正常調用它。
三、注意事項
1.寄存器約定:不同的編譯器和平臺對寄存器的使用有不同的約定,在編寫匯編代碼時需要遵循這些約定。
2.數據類型對齊:在混合編程中,要注意C語言數據類型與匯編指令操作數之間的對齊。
二、參數傳遞陷阱:圖解 cdecl 調用約定堆棧平衡過程
(一)cdecl 調用約定概述
? 在 cdecl 調用約定下,函數的參數是從右到左壓入堆棧的,調用者負責平衡堆棧。這種調用約定比較靈活,允許函數有可變參數列表,但需要調用者在調用函數后清理堆棧。
(二)堆棧平衡過程圖解
初始狀態:
+----------------+
| | <- SP(堆棧指針)
+----------------+1. 壓入參數 b(值為 20)
+----------------+
| 20 | <- SP(堆棧指針)
+----------------+2. 壓入參數 a(值為 10)
+----------------+
| 10 | <- SP(堆棧指針)
+----------------+
| 20 |
+----------------+3. 調用 add 函數,壓入返回地址
+----------------+
| 0x00401000 | <- SP(堆棧指針)
+----------------+
| 10 |
+----------------+
| 20 |
+----------------+4. 函數內部執行,返回值存儲在 eax 寄存器中
+----------------+
| 0x00401000 | <- SP(堆棧指針)
+----------------+
| 10 |
+----------------+
| 20 |
+----------------+5. 函數返回,彈出返回地址
+----------------+
| 10 | <- SP(堆棧指針)
+----------------+
| 20 |
+----------------+6. 彈出參數 a(值為 10)
+----------------+
| 20 | <- SP(堆棧指針)
+----------------+7. 彈出參數 b(值為 20)
+----------------+
| | <- SP(堆棧指針)
+----------------+
1.函數調用前堆棧狀態
? 假設有一個函數 int add(int a, int b),調用代碼為 int result = add(10, 20);。
? 在調用前,堆棧是空的。
2.參數壓棧過程
? 首先,將參數 b(值為 20)壓入堆棧,堆棧指針(SP)向下移動。
? 然后,將參數 a(值為 10)壓入堆棧,堆棧指針繼續向下移動。
? 此時堆棧狀態為:從堆棧頂部開始,依次是 10(a 的值)、20(b 的值)。
3.函數調用與返回
? 調用 add 函數時,函數地址被壓入堆棧,然后跳轉到函數代碼執行。
? 在函數內部,通過堆棧指針訪問參數 a 和 b,執行加法操作,將結果存儲在某個寄存器或內存位置。
? 函數返回時,返回值被存儲在約定的位置(如 eax 寄存器),然后返回指令將控制權交還給調用者。
4.堆棧平衡
? 調用者在函數返回后,需要清理堆棧。它將堆棧指針向上移動,彈出之前壓入的參數(10 和 20),恢復堆棧到調用前的狀態。
? 如果調用者忘記清理堆棧,會導致堆棧指針混亂,可能引發程序崩潰等嚴重錯誤。
三、混合調試技巧
(一)使用 objdump 解析混合目標文件符號
1.objdump 工具簡介
? objdump 是一個用于顯示目標文件信息的工具,它可以顯示符號表、反匯編代碼等內容。
2.解析混合目標文件符號
? 假設有一個混合了 C 和匯編代碼的目標文件 mixed.o。
? 使用命令 objdump -t mixed.o 可以查看符號表,它會列出所有的符號(包括 C 函數名、全局變量名、匯編標簽等)及其地址。
? 通過符號表,可以了解 C 函數匯和編代碼之間的關聯,比如某個 C 函數的入口地址對應匯編代碼的哪個部分,這對于調試混合代碼非常有幫助。
(二)在 GDB 中同時顯示 C 源碼與對應匯編
1.GDB 簡介
? GDB(GNU Debugger)是一個功能強大的調試工具,可以用于調試 C、C++ 等語言編寫的程序。
2.同時顯示 C 源碼與匯編
? 在 GDB 中,可以使用 disassemble 命令查看當前函數的匯編代碼。
? 同時,使用 list 命令可以查看 C 源碼。
? 通過結合這兩個命令,可以在調試過程中同時查看 C 源碼和對應的匯編代碼,方便理解程序的執行過程。例如,當程序執行到某個 C 語言函數時,先用 list 查看該函數的源碼,然后用 disassemble 查看對應的匯編代碼,分析匯編代碼是如何實現 C 語言功能的。