• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            posts - 319, comments - 22, trackbacks - 0, articles - 11
              C++博客 :: 首頁 :: 新隨筆 :: 聯系 :: 聚合  :: 管理

            函數調用約定和堆棧(轉載)

            Posted on 2011-05-12 22:00 RTY 閱讀(462) 評論(0)  編輯 收藏 引用 所屬分類: 編程常識轉載隨筆

            1 什么是堆棧

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

            函數調用經常是嵌套的,在同一時刻,堆棧中會有多個函數的信息,每個函數占用一個連續的區域。一個函數占用的區域被稱作幀(frame)。

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

            在多線程(任務)環境,CPU的堆棧指針指向的存儲器區域就是當前使用的堆棧。切換線程的一個重要工作,就是將堆棧指針設為當前線程的堆棧棧頂地址。

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

            2 函數調用約定

            函數調用約定包括傳遞參數的順序,誰負責清理參數占用的堆棧等,例如 :

             參數傳遞順序誰負責清理參數占用的堆棧
            __pascal從左到右調用者
            __stdcall從右到左被調函數
            __cdecl從右到左調用者

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

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

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

            3 例子:__cdecl和__stdcall

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

            VC++編譯器的已經不再支持__pascal, __fortran, __syscall等函數調用約定。目前只支持__cdecl和__stdcall。

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

            如前表所示,__cdecl和__stdcall的區別是:__cdecl是調用者清理參數占用的堆棧,__stdcall是被調函數清理參數占用的堆棧。

            由于__stdcall的被調函數在編譯時就必須知道傳入參數的準確數目(被調函數要清理堆棧),所以不能支持變參數函數,例如printf。而且如果調用者使用了不正確的參數數目,會導致堆棧錯誤。

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

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

            對應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的函數調用則不需要調整堆棧:

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

            函數

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

            產生以下匯編代碼(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 // 跳轉到esp所指地址,并將esp+4,使esp指向進入函數時的第一個參數

            再查看__stdcall函數的實現,會發現與__cdecl函數只有最后一行不同:

              ret 8 // 執行ret并清理參數占用的堆棧

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

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

            產生以下匯編代碼:

              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不等于函數調用前保存的值,就會轉到錯誤處理代碼。

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

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

            3 補充說明

            函數調用約定只是“調用函數的代碼”和被調用函數之間的關系。

            假設函數A是__stdcall,函數B調用函數A。你必須通過函數聲明告訴編譯器,函數A是__stdcall。編譯器自然會產生正確的調用代碼。

            如果函數A是__stdcall。但在引用函數A的地方,你卻告訴編譯器,函數A是__cdecl方式,編譯器產生__cdecl方式的代碼,與函數A的調用約定不一致,就會發生錯誤。

            以delphi調用VC函數為例,delphi的函數缺省采用__pascal約定,VC的函數缺省采用__cdecl約定。我們一般將VC的函數設為__stdcall,例如:

              int __stdcall add(int a, int b);

            在delphi中將這個函數也聲明為__stdcall,就可以調用了:

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

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

            国产福利电影一区二区三区久久久久成人精品综合 | 久久婷婷五月综合成人D啪| 国产成人久久精品一区二区三区 | 波多野结衣中文字幕久久| 人妻丰满AV无码久久不卡| 成人亚洲欧美久久久久| 一极黄色视频久久网站| 国产精品美女久久久久| 久久激情亚洲精品无码?V| 亚洲AV无码一区东京热久久 | 久久综合久久综合亚洲| 国产成人精品免费久久久久| 久久精品国产亚洲Aⅴ香蕉| 久久精品国产亚洲精品2020| 久久精品国产一区二区三区不卡 | 中文成人无码精品久久久不卡| 精品多毛少妇人妻AV免费久久 | 久久久久这里只有精品| 香蕉久久av一区二区三区| 国产成人精品久久亚洲| 久久精品中文无码资源站| 久久青青国产| 亚洲天堂久久精品| 久久狠狠色狠狠色综合| 婷婷久久香蕉五月综合加勒比| 香蕉aa三级久久毛片| 无码国内精品久久人妻麻豆按摩| 欧美精品一区二区精品久久| 久久精品国产精品亚洲毛片| 一本一本久久aa综合精品| 无码国内精品久久综合88 | 久久久久亚洲av成人网人人软件| 久久高清一级毛片| 亚洲国产精品久久久久| 国产午夜久久影院| 久久精品成人免费看| 精品久久久久久亚洲| 亚洲狠狠综合久久| 国产福利电影一区二区三区久久老子无码午夜伦不 | 国产成人精品久久一区二区三区av| 久久国产视屏|