# CHAPER7 訪問傳遞參數
現在我們來看函數調用者通過棧把參數傳遞到被調用函數。被調用函數是如何訪問這些參數呢?
```
#!cpp
#include <stdio.h>
int f (int a, int b, int c)
{
return a*b+c;
};
int main()
{
printf ("%d
", f(1, 2, 3));
return 0;
};
```
## 7.1 X86
### 7.1.1 MSVC
如下為相應的反匯編代碼(MSVC 2010 Express)
Listing 7.2 MSVC 2010 Express
```
#!bash
_TEXT SEGMENT
_a$ = 8 ; size = 4
_b$ = 12 ; size = 4
_c$ = 16 ; size = 4
_f PROC
push ebp
mov ebp, esp
mov eax, DWORD PTR _a$[ebp]
imul eax, DWORD PTR _b$[ebp]
add eax, DWORD PTR _c$[ebp]
pop ebp
ret 0
_f ENDP
_main PROC
push ebp
mov ebp, esp
push 3 ; 3rd argument
push 2 ; 2nd argument
push 1 ; 1st argument
call _f
add esp, 12
push eax
push OFFSET $SG2463 ; ’%d’, 0aH, 00H
call _printf
add esp, 8
; return 0
xor eax, eax
pop ebp
ret 0
_main ENDP
```
我們可以看到函數main()中3個數字被圧棧,然后函數f(int, int, int)被調用。函數f()內部訪問參數時使用了像_ a$=8 的宏,同樣,在函數內部訪問局部變量也使用了類似的形式,不同的是訪問參數時偏移值(為正值)。因此EBP寄存器的值加上宏_a$的值指向壓棧參數。
`_a$[ebp]`的值被存儲在寄存器eax中,IMUL指令執行后,eax的值為eax與`_b$[ebp]`的乘積,然后eax與`_c$[ebp]`的值相加并將和放入eax寄存器中,之后返回eax的值。返回值作為printf()的參數。
### 7.1.2 MSVC+OllyDbg
我們在OllyDbg中觀察,跟蹤到函數f()使用第一個參數的位置,可以看到寄存器EBP指向棧底,圖中使用紅色箭頭標識。棧幀中第一個被保存的是EBP的值,第二個是返回地址(RA),第三個是參數1,接下來是參數2,以此類推。因此,當我們訪問第一個參數時EBP應該加8(2個32-bit字節寬度)。

Figure 7.1: OllyDbg: 函數f()內部
### 7.1.3 GCC
使用GCC4.4.1編譯后在IDA中查看
Listing 7.3: GCC 4.4.1
```
#!bash
public f
f proc near
arg_0 = dword ptr 8
arg_4 = dword ptr 0Ch
arg_8 = dword ptr 10h
push ebp
mov ebp, esp
mov eax, [ebp+arg_0] ; 1st argument
imul eax, [ebp+arg_4] ; 2nd argument
add eax, [ebp+arg_8] ; 3rd argument
pop ebp
retn
f endp
public main
main proc near
var_10 = dword ptr -10h
var_C = dword ptr -0Ch
var_8 = dword ptr -8
push ebp
mov ebp, esp
and esp, 0FFFFFFF0h
sub esp, 10h
mov [esp+10h+var_8], 3 ; 3rd argument
mov [esp+10h+var_C], 2 ; 2nd argument
mov [esp+10h+var_10], 1 ; 1st argument
call f
mov edx, offset aD ; "%d
"
mov [esp+10h+var_C], eax
mov [esp+10h+var_10], edx
call _printf
mov eax, 0
leave
retn
main endp
```
幾乎相同的結果。
執行兩個函數后棧指針ESP并沒有顯示恢復,因為倒數第二個指令LEAVE(B.6.2)會自動恢復棧指針。
## 7.2 X64
x86-64架構下有點不同,函數參數(4或6)使用寄存器傳遞,被調用函數通過訪問寄存器來訪問傳遞進來的參數。
### 7.2.1 MSVC
MSVC優化后:
Listing 7.4: MSVC 2012 /Ox x64
```
#!bash
$SG2997 DB ’%d’, 0aH, 00H
main PROC
sub rsp, 40
mov edx, 2
lea r8d, QWORD PTR [rdx+1] ; R8D=3
lea ecx, QWORD PTR [rdx-1] ; ECX=1
call f
lea rcx, OFFSET FLAT:$SG2997 ; ’%d’
mov edx, eax
call printf
xor eax, eax
add rsp, 40
ret 0
main ENDP
f PROC
; ECX - 1st argument
; EDX - 2nd argument
; R8D - 3rd argument
imul ecx, edx
lea eax, DWORD PTR [r8+rcx]
ret 0
f ENDP
```
我們可以看到函數f()直接使用寄存器來操作參數,LEA指令用來做加法,編譯器認為使用LEA比使用ADD指令要更快。在mian()中也使用了LEA指令,編譯器認為使用LEA比使用MOV指令效率更高。
我們來看看MSVC沒有優化的情況:
Listing 7.5: MSVC 2012 x64
```
#!bash
f proc near
; shadow space:
arg_0 = dword ptr 8
arg_8 = dword ptr 10h
arg_10 = dword ptr 18h
; ECX - 1st argument
; EDX - 2nd argument
; R8D - 3rd argument
mov [rsp+arg_10], r8d
mov [rsp+arg_8], edx
mov [rsp+arg_0], ecx
mov eax, [rsp+arg_0]
imul eax, [rsp+arg_8]
add eax, [rsp+arg_10]
retn
f endp
main proc near
sub rsp, 28h
mov r8d, 3 ; 3rd argument
mov edx, 2 ; 2nd argument
mov ecx, 1 ; 1st argument
call f
mov edx, eax
lea rcx, $SG2931 ; "%d
"
call printf
; return 0
xor eax, eax
add rsp, 28h
retn
main endp
```
這里從寄存器傳遞進來的3個參數因為某種情況又被保存到棧里。這就是所謂的“shadow space”2:每個Win64通常(不是必需)會保存所有4個寄存器的值。這樣做由兩個原因:1)為輸入參數分配所有寄存器(即使是4個)太浪費,所以要通過堆棧來訪問;2)每次中斷下來調試器總是能夠定位函數參數3。
調用者負責在棧中分配“shadow space”。
### 7.2.2 GCC
GCC優化后的代碼:
Listing 7.6: GCC 4.4.6 -O3 x64
```
#!bash
f:
; EDI - 1st argument
; ESI - 2nd argument
; EDX - 3rd argument
imul esi, edi
lea eax, [rdx+rsi]
ret
main:
sub rsp, 8
mov edx, 3
mov esi, 2
mov edi, 1
call f
mov edi, OFFSET FLAT:.LC0 ; "%d
"
mov esi, eax
xor eax, eax ; number of vector registers passed
call printf
xor eax, eax
add rsp, 8
ret
```
GCC無優化代碼:
Listing 7.7: GCC 4.4.6 x64
```
#!bash
f:
; EDI - 1st argument
; ESI - 2nd argument
; EDX - 3rd argument
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
mov DWORD PTR [rbp-12], edx
mov eax, DWORD PTR [rbp-4]
imul eax, DWORD PTR [rbp-8]
add eax, DWORD PTR [rbp-12]
leave
ret
main:
push rbp
mov rbp, rsp
mov edx, 3
mov esi, 2
mov edi, 1
call f
mov edx, eax
mov eax, OFFSET FLAT:.LC0 ; "%d
"
mov esi, edx
mov rdi, rax
mov eax, 0 ; number of vector registers passed
call printf
mov eax, 0
leave
ret
```
System V *NIX [21]沒有“shadow space”,但被調用者可能會保存參數,這也是造成寄存器短缺的原因。
### 7.2.3 GCC: uint64_t instead int
我們例子使用的是32位int,寄存器也為32位寄存器(前綴為E-)。
為處理64位數值內部會自動調整為64位寄存器:
```
#!cpp
#include <stdio.h>
#include <stdint.h>
uint64_t f (uint64_t a, uint64_t b, uint64_t c)
{
return a*b+c;
};
int main()
{
printf ("%lld
", f(0x1122334455667788,0x1111111122222222,0x3333333344444444));
return 0;
};
```
Listing 7.8: GCC 4.4.6 -O3 x64
```
#!cpp
f proc near
imul rsi, rdi
lea rax, [rdx+rsi]
retn
f endp
main proc near
sub rsp, 8
mov rdx, 3333333344444444h ; 3rd argument
mov rsi, 1111111122222222h ; 2nd argument
mov rdi, 1122334455667788h ; 1st argument
call f
mov edi, offset format ; "%lld
"
mov rsi, rax
xor eax, eax ; number of vector registers passed
call _printf
xor eax, eax
add rsp, 8
retn
main endp
```
代碼非常相似,只是使用了64位寄存器(前綴為R)。
## 7.3 ARM
### 7.3.1 未優化的Keil + ARM mode
```
#!bash
.text:000000A4 00 30 A0 E1 MOV R3, R0
.text:000000A8 93 21 20 E0 MLA R0, R3, R1, R2
.text:000000AC 1E FF 2F E1 BX LR
...
.text:000000B0 main
.text:000000B0 10 40 2D E9 STMFD SP!, {R4,LR}
.text:000000B4 03 20 A0 E3 MOV R2, #3
.text:000000B8 02 10 A0 E3 MOV R1, #2
.text:000000BC 01 00 A0 E3 MOV R0, #1
.text:000000C0 F7 FF FF EB BL f
.text:000000C4 00 40 A0 E1 MOV R4, R0
.text:000000C8 04 10 A0 E1 MOV R1, R4
.text:000000CC 5A 0F 8F E2 ADR R0, aD_0 ; "%d
"
.text:000000D0 E3 18 00 EB BL __2printf
.text:000000D4 00 00 A0 E3 MOV R0, #0
.text:000000D8 10 80 BD E8 LDMFD SP!, {R4,PC}
```
main()函數里調用了另外兩個函數,3個值被傳遞到f();
正如前面提到的,ARM通常使用前四個寄存器(R0-R4)傳遞前四個值。
f()函數使用了前三個寄存器(R0-R2)作為參數。
MLA (Multiply Accumulate)指令將R3寄存器和R1寄存器的值相乘,然后再將乘積與R2寄存器的值相加將結果存入R0,函數返回R0。
一條指令完成乘法和加法4,如果不包括SIMD新的FMA指令5,通常x86下沒有這樣的指令。
第一條指令MOV R3,R0,看起來冗余是因為該代碼是非優化的。
BX指令返回到LR寄存器存儲的地址,處理器根據狀態模式從Thumb狀態轉換到ARM狀態,或者反之。函數f()可以被ARM代碼或者Thumb代碼調用,如果是Thumb代碼調用BX將返回到調用函數并切換到Thumb模式,或者反之。
### 7.3.2 Optimizing Keil + ARM mode
```
#!bash
.text:00000098 f
.text:00000098 91 20 20 E0 MLA R0, R1, R0, R2
.text:0000009C 1E FF 2F E1 BX LR
```
這里f()編譯時使用完全優化模式(-O3),MOV指令被優化,現在MLA使用所有輸入寄存器并將結果置入R0寄存器。
### 7.3.3 Optimizing Keil + thumb mode
```
#!bash
.text:0000005E 48 43 MULS R0, R1
.text:00000060 80 18 ADDS R0, R0, R2
.text:00000062 70 47 BX LR
```
Thumb模式下沒有MLA指令,編譯器做了兩次間接處理,MULS指令使R0寄存器的值與R1寄存器的值相乘并將結果存入R0。ADDS指令將R0與R2的值相加并將結果存入R0。
- 第一章 CPU簡介
- 第二章 Hello,world!
- 第三章? 函數開始和結束
- 第四章 棧
- Chapter 5 printf() 與參數處理
- Chapter 6 scanf()
- CHAPER7 訪問傳遞參數
- Chapter 8 一個或者多個字的返回值
- Chapter 9 指針
- Chapter 10 條件跳轉
- 第11章 選擇結構switch()/case/default
- 第12章 循環結構
- 第13章 strlen()
- Chapter 14 Division by 9
- chapter 15 用FPU工作
- Chapter 16 數組
- Chapter 17 位域
- 第18章 結構體
- 19章 聯合體
- 第二十章 函數指針
- 第21章 在32位環境中的64位值
- 第二十二章 SIMD
- 23章 64位化
- 24章 使用x64下的SIMD來處理浮點數
- 25章 溫度轉換
- 26章 C99的限制
- 27章 內聯函數
- 第28章 得到不正確反匯編結果
- 第29章 花指令
- 第30章 16位Windows
- 第31章 類
- 三十二 ostream