<span id="7ztzv"></span>
<sub id="7ztzv"></sub>

<span id="7ztzv"></span><form id="7ztzv"></form>

<span id="7ztzv"></span>

        <address id="7ztzv"></address>

            原文地址:http://drops.wooyun.org/binary/7428

            64章 傳遞參數的方法


            64.1 cdcel


            這種傳遞參數的方法在C/C++語言里面比較流行。

            如下的代碼片段所示,調用者反序地把參數壓到棧中:最后一個參數,倒數第二個參數,第一個參數。調用者還必須在函數返回之后把棧指針(ESP)還原為初始狀態。

            Listing 64.1: cdecl

            push arg3
            push arg2
            push arg1
            call function
            add esp, 12 ; returns ESP
            

            64.2 stdcall


            該調用方法與cdecl差不多,除了被調用者必須通過RET x指令代替RET指令將ESP指針設置為初始化狀態,其中x = arguments number * sizeof(int)。調用者無需調整棧指針(ESP)。

            Listing 64.2: stdcall

            push arg3
            push arg2
            push arg1
            call function
            function:
            ... do something ...
            ret 12
            

            這種調用方式在win32的標準庫無處不在,但win64并不使用該調用方法(具體參見下文win64一節)。

            舉個例子,我們可以稍微把在91頁中8.1的示例代碼修改一下,增加一個__stdcall修飾符。

            #!c
            int __stdcall f2 (int a, int b, int c)
            {
                return a*b+c;
            };
            

            編譯出來的結果跟8.2幾乎一模一樣,但你可以看到它是通過RET 12而不是RET返回的。同時,調用者并沒有調整棧指針(ESP)。

            因此,很容易通過RETN n指令推導出函數參數的數量(n除以四)。

            Listing 64.3: MSVC 2010

            _a$ = 8 ; size = 4
            _b$ = 12 ; size = 4
            _c$ = 16 ; size = 4
            [email protected] 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 12 ; 0000000cH
            [email protected] ENDP
            ; ...
                push 3
                push 2
                push 1
                call [email protected]
                push eax
                push OFFSET $SG81369
                call _printf
                add esp, 8
            

            64.2.1 可變參數的函數

            printf()系列的函數大概是C/C++里面唯一一系列具有可變參數的函數了,在這些函數的幫助下很容易理清cdecl和stdcall兩種調用方式之間的重要區別。讓我們先假設編譯器知道每個調用printf()函數的參數的個數,無論如何,當我們調用printf()的時候,它已經存在于編譯好的MSVCRT.DLL之中(我們討論的是Windows),并沒有任何關于傳遞多少個參數的信息,剩下的辦法就是通過它的格式字符串獲取得到參數個數。因此,如果printf()函數是一個stdcall調用方式的函數,它必須通過格式字符串計算參數個數用于恢復棧指針(ESP),這是一種相當危險的情況,程序員的一個錯別字就可以導致程序崩潰。因此此類函數使用cdecl調用方式遠比使用stdcall調用方式適合。

            64.3 fastcall


            這是一種將部分參數通過寄存器傳入,其余參數通過棧方式傳入的方法。它的執行效率在一些舊時CPU比cdecl/stdcall要好(因為小棧的壓力)。然而,在現代的CPU中使用該調用方式不一定能獲得更好的性能。

            fastcall并沒有一個標準化,因此不同的編譯器的實現可以不同。這是一個眾所周知的警告:如果你有兩個DLL,其中第一個DLL調用第二個DLL的函數,它們是又分別不同的編譯器使用fastcall調用方式編譯出來的,則會有不可預期的后果。

            MSVC和GCC兩個編譯器都是通過ECX和EDX來傳遞第一個和第二個參數,通過棧進行傳遞其余參數。棧指針必須被被調用者恢復為初始狀態(與stdcall類似)。

            Listing 64.4: fastcall

            push arg3
            mov edx, arg2
            mov ecx, arg1
            call function
            function:
            .. do something ..
            ret 4
            

            舉個例子,我們可以稍微把8.1的示例代碼修改一下,增加一個__fastcall修飾符。

            #!c
            int __fastcall f3 (int a, int b, int c)
            {
                return a*b+c;
            };
            

            下面它編譯出來的結果:

            Listing 64.5: Optimizing MSVC 2010 /Ob0

            _c$ = 8         ; size = 4
            @[email protected] PROC
            ; _a$ = ecx
            ; _b$ = edx
                mov eax, ecx
                imul eax, edx
                add eax, DWORD PTR _c$[esp-4]
                ret 4
            @[email protected] ENDP
            ; ...
                mov edx, 2
                push 3
                lea ecx, DWORD PTR [edx-1]
                call @[email protected]
                push eax
                push OFFSET $SG81390
                call _printf
                add esp, 8
            

            我們可以看到被調用者使用RET N指令來調整棧指針(ESP)。這意味著,我們可以通過這條指令來推斷出參數的個數。

            64.3.1 GCC regparm

            這是一種對fastcall調用方式的某種優化。使用-mregparm編譯選項可以設置多少個參數是通過寄存器傳遞的(最大為3個)。因此,EAX,EDX和ECX寄存器將被使用。

            當然,如果指定通過寄存器傳參的參數數量小于三個的時候,并沒有使用完這三個寄存器。

            調用者需要把棧指針恢復為初始狀態。

            相關例子請參看(19.1.1)。

            64.3.2 Watcom/OpenWatcom 編譯器

            在這里,它被成為“寄存器調用約定”,頭四個參數通過EAX,EDX,EBX和ECX傳遞。其余參數通過棧傳遞。通過在函數名上添加下劃線來區分那些不同的調用約定。

            64.4 thiscall


            這是C++里面傳遞this指針的成員函數調用約定。

            在MSVC里面,this指針通過ECX寄存器來傳遞。

            在GCC里面,this指針是通過第一個參數進行傳遞的。因此很明顯,在所有成員函數里面都會多出一個額外的參數。

            相關例子請查看(51.1.1)。

            64.5 x86-64


            64.5.1 Windows x64

            在Win64里面傳遞函數參數的方法類似fastcall調用約定。前四個參數通過RCX,RDX,R8和R9寄存器傳參,其余參數通過棧進行傳遞。調用者還必須預留32個字節或者4個64位的空間,讓被調用者可以保存前四個參數。短函數可能直接使用通過寄存器傳過來的值,但更大的可能是保存那些值后在進一步使用。

            調用者還必須負責還原棧指針。

            這個調用約定也用于Windows x86-64位系統上的DLL(而不是Win32的stdcall)。

            例子

            #!c
            #include <stdio.h>
            void f1(int a, int b, int c, int d, int e, int f, int g)
            {
                printf ("%d %d %d %d %d %d %d\n", a, b, c, d, e, f, g);
            };
            int main()
            {
                f1(1,2,3,4,5,6,7);
            };
            

            Listing 64.6: MSVC 2012 /0b

            $SG2937 DB '%d %d %d %d %d %d %d', 0aH, 00H
            main PROC
                sub rsp, 72                     ; 00000048H
                mov DWORD PTR [rsp+48], 7
                mov DWORD PTR [rsp+40], 6
                mov DWORD PTR [rsp+32], 5
                mov r9d, 4
                mov r8d, 3
                mov edx, 2
                mov ecx, 1
                call f1
                xor eax, eax
                add rsp, 72                     ; 00000048H
                ret 0
            main ENDP
            a$ = 80
            b$ = 88
            c$ = 96
            d$ = 104
            e$ = 112
            f$ = 120
            g$ = 128
            f1 PROC
            $LN3:
                mov DWORD PTR [rsp+32], r9d
                mov DWORD PTR [rsp+24], r8d
                mov DWORD PTR [rsp+16], edx
                mov DWORD PTR [rsp+8], ecx
                sub rsp, 72                     ; 00000048H
                mov eax, DWORD PTR g$[rsp]
                mov DWORD PTR [rsp+56], eax
                mov eax, DWORD PTR f$[rsp]
                mov DWORD PTR [rsp+48], eax
                mov eax, DWORD PTR e$[rsp]
                mov DWORD PTR [rsp+40], eax
                mov eax, DWORD PTR d$[rsp]
                mov DWORD PTR [rsp+32], eax
                mov r9d, DWORD PTR c$[rsp]
                mov r8d, DWORD PTR b$[rsp]
                mov edx, DWORD PTR a$[rsp]
                lea rcx, OFFSET FLAT:$SG2937
                call printf
                add rsp, 72                     ; 00000048H
                ret 0
                f1 ENDP
            

            在這里我們可以清楚看到這7個參數是如何傳遞的:4個參數通過寄存器傳遞而其余3個通過棧傳遞。f1()的反匯編代碼一開始就把參數保存到“預留”的棧空間之中,這樣做的目的是編譯器并不能保證有足夠的寄存器可以使用,如果不這樣做的話這四個寄存器將被參數占用到函數執行結束。最后,預留棧空間是調用者的職責。

            Listing 64.7: Optimizing MSVC 2012 /0b

            $SG2777 DB '%d %d %d %d %d %d %d', 0aH, 00H
            a$ = 80
            b$ = 88
            c$ = 96
            d$ = 104
            e$ = 112
            f$ = 120
            g$ = 128
            f1 PROC
            $LN3:
                sub rsp, 72                     ; 00000048H
                mov eax, DWORD PTR g$[rsp]
                mov DWORD PTR [rsp+56], eax
                mov eax, DWORD PTR f$[rsp]
                mov DWORD PTR [rsp+48], eax
                mov eax, DWORD PTR e$[rsp]
                mov DWORD PTR [rsp+40], eax
                mov DWORD PTR [rsp+32], r9d
                mov r9d, r8d
                mov r8d, edx
                mov edx, ecx
                lea rcx, OFFSET FLAT:$SG2777
                call printf
                add rsp, 72                     ; 00000048H
                ret 0
            f1 ENDP
            main PROC
                sub rsp, 72                     ; 00000048H
                mov edx, 2
                mov DWORD PTR [rsp+48], 7
                mov DWORD PTR [rsp+40], 6
                lea r9d, QWORD PTR [rdx+2]
                lea r8d, QWORD PTR [rdx+1]
                lea ecx, QWORD PTR [rdx-1]
                mov DWORD PTR [rsp+32], 5
                call f1
                xor eax, eax
                add rsp, 72                     ; 00000048H
                ret 0
            main ENDP
            

            如果我們使用了編譯優化的開關去編譯上面的例子,它的反匯編碼幾乎是相同的,但是預留的棧空間將不被使用,因為在這里并不需要使用到預留的棧空間。

            而且可以看到MSVC 2012是如何利用LEA指令來優化代碼(A.6.2)。

            我也不確定是否值得這么做。

            更多的例子請看(74.1)

            this指針的傳遞(C/C++)

            this指針通過RCX傳遞,成員函數的第一個參數通過RDX傳遞,更多例子請看(51.1.1)。

            64.5.2 Linux x64

            Linux x86-64傳遞參數的方式幾乎和Windows一樣。但是是通過6個寄存器代替4個寄存器來傳參(RDI,RSI,RDX,RCX,R8,R9),另外并沒有預留的棧空間這回事。雖然,如果它需要/想要的話,可以把寄存器的值保存到棧之中。

            Listing 64.8: Optimizing GCC 4.7.3

            .LC0:
                .string "%d %d %d %d %d %d %d\n"
            f1:
                sub rsp, 40
                mov eax, DWORD PTR [rsp+48]
                mov DWORD PTR [rsp+8], r9d
                mov r9d, ecx
                mov DWORD PTR [rsp], r8d
                mov ecx, esi
                mov r8d, edx
                mov esi, OFFSET FLAT:.LC0
                mov edx, edi
                mov edi, 1
                mov DWORD PTR [rsp+16], eax
                xor eax, eax
                call __printf_chk
                add rsp, 40
                ret
            main:
                sub rsp, 24
                mov r9d, 6
                mov r8d, 5
                mov DWORD PTR [rsp], 7
                mov ecx, 4
                mov edx, 3
                mov esi, 2
                mov edi, 1
                call f1
                add rsp, 24
                ret
            

            注意:這里的值是寫入到32-bit的寄存器(EAX...)而不是整個64-bit寄存器(RAX...)。這是因為寫入到32-bit寄存器的時候會自動清空高32-bit。據說,這是為了方便把代碼移植到x86-64。

            64.6 返回float和double類型的值


            除了Win64之外,其它返回float和double類型的值都是通過FPU里面的ST(0)寄存器返回的。 在Win64里面,返回float和double類型的值是通過XMM0寄存器返回。

            64.7 修改參數


            有時候,C/C++程序員(雖然不僅僅是這些人)可能會問,如果他們碰巧修改了參數會怎樣?答案非常簡單,這些參數是保存在棧里面的,修改參數的時候是修改這個棧里面的內容,調用者并沒有在被調用函數退出之后再使用它們(至少在我的實踐中沒有遇到這種情況)。

            #!c
            #include <stdio.h>
            void f(int a, int b)
            {
                a=a+b;
                printf ("%d\n", a);
            };
            

            Listing 64.9: MSVC 2012

            _a$ = 8     ; size = 4
            _b$ = 12    ; size = 4
            _f PROC
                push ebp
                mov ebp, esp
                mov eax, DWORD PTR _a$[ebp]
                add eax, DWORD PTR _b$[ebp]
                mov DWORD PTR _a$[ebp], eax
                mov ecx, DWORD PTR _a$[ebp]
                push ecx
                push OFFSET $SG2938 ; '%d', 0aH
                call _printf
                add esp, 8
                pop ebp
                ret 0
            _f END
            

            是的,可以隨便修改參數。當然,這得它不是C++的引用(references)(51.3),而且你如果不修改通過指針指向的數據。那么修改參數是不會影響到當前函數的。

            從理論上來講,被調用者的函數返回之后,調用者可以獲取并修改和使用它。如果它是直接使用匯編語言編寫的。但C/C++并不提供任何方式可以訪問它們。

            64.8 使用指針的函數參數


            ...更有意思的是,有可能在程序中,取一個函數參數的指針并將其傳遞給另外一個函數。

            #!c
            #include <stdio.h>
            // located in some other file
            void modify_a (int *a);
            void f (int a)
            {
                modify_a (&a);
                printf ("%d\n", a);
            };
            

            很難理解它是如果實現的,直到我們看到它的反匯編碼:

            Listing 64.10: Optimizing MSVC 2010

            $SG2796 DB '%d', 0aH, 00H
            _a$ = 8
            _f PROC
                lea eax, DWORD PTR _a$[esp-4]   ; just get the address of value in local stack
                push eax                        ; and pass it to modify_a()
                call _modify_a
                mov ecx, DWORD PTR _a$[esp]     ; reload it from the local stack
                push ecx                        ; and pass it to printf()
                push OFFSET $SG2796             ; '%d'
                call _printf
                add esp, 12
                ret 0
            _f ENDP
            

            傳遞到另一個函數是a在棧空間上的地址,該函數修改了指針指向的值然后再調用printf()來打印出修改之后的值。

            細心的讀者可能會問,使用寄存器傳參的調用約定是如何傳遞函數指針參數的?

            這是一種利用了影子空間的情況,輸入的參數值先從寄存器復制到局部棧中的影子空間,然后再講這個地址傳遞給其他函數。

            Listing 64.11: Optimizing MSVC 2012 x64

            $SG2994 DB '%d', 0aH, 00H
            a$ = 48
            f PROC
                mov DWORD PTR [rsp+8], ecx      ; save input value in Shadow Space
                sub rsp, 40
                lea rcx, QWORD PTR a$[rsp]      ; get address of value and pass it to modify_a()
                call modify_a
                mov edx, DWORD PTR a$[rsp]      ; reload value from Shadow Space and pass it to printf()
                lea rcx, OFFSET FLAT:$SG2994    ; '%d'
                call printf
                add rsp, 40
                ret 0
            f ENDP
            

            GCC同樣將傳入的參數存儲在本地棧空間:

            Listing 64.12: Optimizing GCC 4.9.1 x64

            .LC0:
            .string "%d\n"
            f:
                sub rsp, 24
                mov DWORD PTR [rsp+12], edi     ; store input value to the local stack
                lea rdi, [rsp+12]               ; take an address of the value and pass it to modify_a()
                call modify_a
                mov edx, DWORD PTR [rsp+12]     ; reload value from the local stack and pass it to printf()
                mov esi, OFFSET FLAT:.LC0       ; '%d'
                mov edi, 1
                xor eax, eax
                call __printf_chk
                add rsp, 24
                ret
            

            ARM64的GCC也做了同樣的事情,但這個空間稱為寄存器保護區:

            f:
                stp x29, x30, [sp, -32]!
                add x29, sp, 0      ; setup FP
                add x1, x29, 32     ; calculate address of variable in Register Save Area
                str w0, [x1,-4]!    ; store input value there
                mov x0, x1          ; pass address of variable to the modify_a()
                bl modify_a
                ldr w1, [x29,28]    ; load value from the variable and pass it to printf()
                adrp x0, .LC0 ; '%d'
                add x0, x0, :lo12:.LC0
                bl printf ; call printf()
                ldp x29, x30, [sp], 32
                ret
            .LC0:
                .string "%d\n"
            

            順便提一下,一個類似影子空間的使用在這里也被提及過(46.1.2)。

            <span id="7ztzv"></span>
            <sub id="7ztzv"></sub>

            <span id="7ztzv"></span><form id="7ztzv"></form>

            <span id="7ztzv"></span>

                  <address id="7ztzv"></address>

                      亚洲欧美在线