黄色网页视频 I 影音先锋日日狠狠久久 I 秋霞午夜毛片 I 秋霞一二三区 I 国产成人片无码视频 I 国产 精品 自在自线 I av免费观看网站 I 日本精品久久久久中文字幕5 I 91看视频 I 看全色黄大色黄女片18 I 精品不卡一区 I 亚洲最新精品 I 欧美 激情 在线 I 人妻少妇精品久久 I 国产99视频精品免费专区 I 欧美影院 I 欧美精品在欧美一区二区少妇 I av大片网站 I 国产精品黄色片 I 888久久 I 狠狠干最新 I 看看黄色一级片 I 黄色精品久久 I 三级av在线 I 69色综合 I 国产日韩欧美91 I 亚洲精品偷拍 I 激情小说亚洲图片 I 久久国产视频精品 I 国产综合精品一区二区三区 I 色婷婷国产 I 最新成人av在线 I 国产私拍精品 I 日韩成人影音 I 日日夜夜天天综合

函數(shù)調(diào)用約定和堆棧

系統(tǒng) 2984 0

函數(shù)調(diào)用約定和堆棧

1 什么是堆棧

編譯器一般使用堆棧實現(xiàn)函數(shù)調(diào)用。堆棧是存儲器的一個區(qū)域,嵌入式環(huán)境有時需要程序員自己定義一個數(shù)組作為堆棧。Windows為每個線程自動維護一個堆棧,堆棧的大小可以設(shè)置。編譯器使用堆棧來堆放每個函數(shù)的參數(shù)、局部變量等信息。

函數(shù)調(diào)用經(jīng)常是嵌套的,在同一時刻,堆棧中會有多個函數(shù)的信息,每個函數(shù)占用一個連續(xù)的區(qū)域。一個函數(shù)占用的區(qū)域被稱作幀(frame)。

函數(shù)調(diào)用約定和堆棧

編譯器從高地址開始使用堆棧。 假設(shè)我們定義一個數(shù)組a[1024]作為堆棧空間,一開始棧頂指針指向a[1023]。如果棧里有兩個函數(shù)a和b,且a調(diào)用了b,棧頂指針會指向函數(shù)b的 幀。如果函數(shù)b返回。棧頂指針就指向函數(shù)a的幀。如果在棧里放了太多東西造成溢出,破壞的是a[0]上面的東西。

在多線程(任務(wù))環(huán)境,CPU的堆棧指針指向的存儲器區(qū)域就是當(dāng)前使用的堆棧。切換線程的一個重要工作,就是將堆棧指針設(shè)為當(dāng)前線程的堆棧棧頂?shù)刂贰?

不同CPU,不同編譯器的堆棧布局、函數(shù)調(diào)用方法都可能不同,但堆棧的基本概念是一樣的。

2 函數(shù)調(diào)用約定

函數(shù)調(diào)用約定包括傳遞參數(shù)的順序,誰負責(zé)清理參數(shù)占用的堆棧等,例如 :

? 參數(shù)傳遞順序 誰負責(zé)清理參數(shù)占用的堆棧
__pascal 從左到右 調(diào)用者
__stdcall 從右到左 被調(diào)函數(shù)
__cdecl 從右到左 調(diào)用者

調(diào)用函數(shù)的代碼和被調(diào)函數(shù)必須采用相同的函數(shù)的調(diào)用約定,程序才能正常運行。在Windows上,__cdecl是C/C++程序的缺省函數(shù)調(diào)用約定。

在有的cpu上,編譯器會用寄存器傳遞參數(shù),函數(shù)使用的堆棧由被調(diào)函數(shù)分配和釋放。這種調(diào)用約定在行為上和__cdecl有一個共同點:實參和形參數(shù)目不符不會導(dǎo)致堆棧錯誤。

不過,即使用寄存器傳遞參數(shù),編譯器在進入函數(shù)時,還是會將寄存器里的參數(shù)存入堆棧指定位置。參數(shù)和局部變量一樣應(yīng)該在堆棧中有一席之地。參數(shù)可以被理解為由調(diào)用函數(shù)指定初值的局部變量。

3 例子:__cdecl和__stdcall

不同的CPU,不同的編譯器,堆棧的布局可能是不同的。本文以x86,VC++的編譯器為例。

VC++編譯器的已經(jīng)不再支持__pascal, __fortran, __syscall等函數(shù)調(diào)用約定。目前只支持__cdecl和__stdcall。

采用__cdecl或__stdcall調(diào)用方式的程序,在剛進入子函數(shù)時,堆棧內(nèi)容是一樣的。esp指向的棧頂是返回地址。這是被call指令壓入堆棧的。下面是參數(shù),左邊參數(shù)在上,右邊參數(shù)在下(先入棧)。

如前表所示,__cdecl和__stdcall的區(qū)別是:__cdecl是調(diào)用者清理參數(shù)占用的堆棧,__stdcall是被調(diào)函數(shù)清理參數(shù)占用的堆棧。

由于__stdcall的被調(diào)函數(shù)在編譯時就必須知道傳入?yún)?shù)的準(zhǔn)確數(shù)目(被調(diào)函數(shù)要清理堆棧),所以不能支持變參數(shù)函數(shù),例如printf。而且如果調(diào)用者使用了不正確的參數(shù)數(shù)目,會導(dǎo)致堆棧錯誤。

通過查看匯編代碼,__cdecl函數(shù)調(diào)用在call語句后會有一個堆棧調(diào)整語句,例如:

    a = 0x1234;
    b = 0x5678;
    c = add(a, b);

對應(yīng)x86匯編:

    mov dword ptr [ebp-4],1234h
    mov dword ptr [ebp-8],5678h
    mov eax,dword ptr [ebp-8]
    push eax
    mov ecx,dword ptr [ebp-4]
    push ecx
    call 0040100a
    add esp,8
    mov dword ptr [ebp-0Ch],eax


__stdcall的函數(shù)調(diào)用則不需要調(diào)整堆棧:

    call 00401005
    mov dword ptr [ebp-0Ch],eax

函數(shù)

    int __cdecl add(int a, int b)
    {
    return a+b;
    }

產(chǎn)生以下匯編代碼(Debug版本):

    push ebp
    mov ebp,esp
    sub esp,40h
    push ebx
    push esi
    push edi
    lea edi,[ebp-40h]
    mov ecx,10h
    mov eax,0CCCCCCCCh
    rep stos dword ptr [edi]
    mov eax,dword ptr [ebp+8]
    add eax,dword ptr [ebp+0Ch]
    pop edi
    pop esi
    pop ebx
    mov esp,ebp
    pop ebp
    ret // 跳轉(zhuǎn)到esp所指地址,并將esp+4,使esp指向進入函數(shù)時的第一個參數(shù)

再查看__stdcall函數(shù)的實現(xiàn),會發(fā)現(xiàn)與__cdecl函數(shù)只有最后一行不同:

    ret 8 // 執(zhí)行ret并清理參數(shù)占用的堆棧

對于調(diào)試版本,VC++編譯器在“直接調(diào)用地址”時會增加檢查esp的代碼,例如:

    ta = (TAdd)add; // TAdd定義:typedef int (__cdecl *TAdd)(int a, int b);
    c = ta(a, b);

產(chǎn)生以下匯編代碼:

    mov [ebp-10h],0040100a
    mov esi,esp
    mov ecx,dword ptr [ebp-8]
    push ecx
    mov edx,dword ptr [ebp-4]
    push edx
    call dword ptr [ebp-10h]
    add esp,8
    cmp esi,esp
    call __chkesp (004011e0)
    mov dword ptr [ebp-0Ch],eax

__chkesp 代碼如下。如果esp不等于函數(shù)調(diào)用前保存的值,就會轉(zhuǎn)到錯誤處理代碼。

    004011E0 jne __chkesp+3 (004011e3)
    004011E2 ret
    004011E3 ;錯誤處理代碼

__chkesp的錯誤處理會彈出對話框,報告函數(shù)調(diào)用造成esp值不正確。 Release版本的匯編代碼要簡潔得多。也不會增加 __chkesp。如果發(fā)生esp錯誤,程序會繼續(xù)運行,直到“遇到問題需要關(guān)閉”。

3 補充說明

函數(shù)調(diào)用約定只是“調(diào)用函數(shù)的代碼”和被調(diào)用函數(shù)之間的關(guān)系。

假設(shè)函數(shù)A是__stdcall,函數(shù)B調(diào)用函數(shù)A。你必須通過函數(shù)聲明告訴編譯器,函數(shù)A是__stdcall。編譯器自然會產(chǎn)生正確的調(diào)用代碼。

如果函數(shù)A是__stdcall。但在引用函數(shù)A的地方,你卻告訴編譯器,函數(shù)A是__cdecl方式,編譯器產(chǎn)生__cdecl方式的代碼,與函數(shù)A的調(diào)用約定不一致,就會發(fā)生錯誤。

以delphi調(diào)用VC函數(shù)為例,delphi的函數(shù)缺省采用__pascal約定,VC的函數(shù)缺省采用__cdecl約定。我們一般將VC的函數(shù)設(shè)為__stdcall,例如:

    int __stdcall add(int a, int b);

在delphi中將這個函數(shù)也聲明為__stdcall,就可以調(diào)用了:

    function add(a: Integer; b: Integer): Integer;
    stdcall; external 'a.dll';

因為考慮到可能被其它語言的程序調(diào)用,不少API采用__stdcall的調(diào)用約定。

函數(shù)調(diào)用約定和堆棧


更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主

微信掃碼或搜索:z360901061

微信掃一掃加我為好友

QQ號聯(lián)系: 360901061

您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。

【本文對您有幫助就好】

您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描上面二維碼支持博主2元、5元、10元、自定義金額等您想捐的金額吧,站長會非常 感謝您的哦!!!

發(fā)表我的評論
最新評論 總共0條評論