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

援引《C++ Primer(Fifth Edition)》4.1.3節(jié):

Order of operand evaluation is independent of precedence and associativity. In an
expression such as f() + g() * h() + j():
? Precedence guarantees that the results of g() and h() are multiplied.
? Associativity guarantees that the result of f() is added to the product of g() and h() and that the result of that addition is added to the value of j().
? There are no guarantees as to the order in which these functions are called.

If f, g, h, and j are independent functions that do not affect the state of the same
objects or perform IO, then the order in which the functions are called is irrelevant. If
any of these functions do affect the same object, then the expression is in error and
has undefined behavior.

大意如下:
操作數(shù)的求職順序與運(yùn)算符的優(yōu)先級(jí)和結(jié)合律無(wú)關(guān)钱磅。在一個(gè)形如f() + g() * h() + j()的表達(dá)式中:

  • 優(yōu)先級(jí)保證g()的返回值與h()的返回值相乘
  • 結(jié)合律保證f()的返回值與g() * h()的結(jié)果相加可柿,并將結(jié)果與j()的返回值相加
  • 但是沒(méi)有任何保證可以確定這些函數(shù)調(diào)用的順序

如果f,g,h,j既沒(méi)有共同關(guān)聯(lián)參數(shù)的(do affect the same object)盹沈,也不是輸入輸出系統(tǒng)(IO)函數(shù)哺哼,那么函數(shù)調(diào)用的順序彼此互不影響。如果其中的任何函數(shù)都引用了相同的對(duì)象,那么這個(gè)表達(dá)式就是錯(cuò)誤的。它會(huì)產(chǎn)生未定義的行為岖瑰。


我們看回題干:

printf("%c%c%c\n",*p++,*p++,*p++);

這里面的突出問(wèn)題在于

  1. 表達(dá)式狀態(tài)共享:

子表達(dá)式共享狀態(tài)p變量

導(dǎo)致一方的求值運(yùn)算會(huì)受另一方結(jié)果影響。

  1. 運(yùn)算對(duì)象求值順序不明:

C++中只有'&&', '||', ',', '?:' 這四個(gè)運(yùn)算符明確了其所屬運(yùn)算對(duì)象的求值順序砂代。

函數(shù)調(diào)用也是一種運(yùn)算符

而實(shí)參壓棧順序完全依賴于編譯器實(shí)現(xiàn)蹋订,三個(gè)*p++求值順序不明。

那么結(jié)合第一個(gè)問(wèn)題刻伊,假如從左向右壓棧結(jié)果就是123

如果換個(gè)編譯器可能順序又不同了

所有選項(xiàng)可能都能有幸成為正確答案

所以露戒,這種表達(dá)式是錯(cuò)誤的,會(huì)產(chǎn)生未定義的行為捶箱。

許多朋友都表達(dá)了一個(gè)廣泛存在的意識(shí)形態(tài):

函數(shù)的參數(shù)從右向左壓入調(diào)用棧

那么參數(shù)表達(dá)式的執(zhí)行順序自然是從右至左

這個(gè)說(shuō)法由來(lái)已久

并且有種“情不知所起 一往而深”的味道

大家(包括我)都是從各種語(yǔ)言書(shū)籍中看到的這種說(shuō)法

不得不說(shuō)這種意識(shí)形態(tài)荼毒甚廣

以至于我在幾篇回答中引用了《C++ Primer(Fifth Edition)》

仍然有不少胖友將信將疑

所以 私以為需要在此開(kāi)宗明義

徹徹底底地論述這個(gè)問(wèn)題的實(shí)質(zhì)

那么就從不同編譯器的調(diào)用約定(calling convention)說(shuō)起吧

讓我們先厘清什么是調(diào)用約定

以下摘自Wikipedia

In computer science, a calling convention is an implementation-level (low-level) scheme for how subroutines receive parameters from their caller and how they return a result. Differences in various implementations include where parameters, return values, return addresses and scope links are placed, and how the tasks of preparing for a function call and restoring the environment afterward are divided between the caller and the callee.

簡(jiǎn)單翻譯如下

在計(jì)算機(jī)科學(xué)領(lǐng)域智什,調(diào)用約定是一種編譯器級(jí)別的方案。這個(gè)方案規(guī)定了函數(shù)如何從它的調(diào)用方獲取實(shí)參以及如何返回函數(shù)結(jié)果丁屎。對(duì)于不同的編譯器而言荠锭,他們的調(diào)用約定的不同之處包括參數(shù)、返回值晨川、返回地址证九、域指針等的存儲(chǔ)位置,如何發(fā)起函數(shù)調(diào)用和調(diào)用結(jié)束后如何恢復(fù)共虑,以及調(diào)用方和被調(diào)方的任務(wù)如何劃分等愧怜。

大家困惑的參數(shù)壓棧順序也是調(diào)用約定的范疇,即上文所言“參數(shù)看蚜、返回值叫搁、返回地址赔桌、域指針等的存儲(chǔ)位置”與“如何發(fā)起函數(shù)調(diào)用和調(diào)用結(jié)束后如何恢復(fù)”

既然調(diào)用約定是編譯器級(jí)別的方案供炎,那么不同編譯器應(yīng)該就有不同的實(shí)現(xiàn)渴逻。

在此我把我研究過(guò)的常見(jiàn)的幾種編譯器的調(diào)用約定分別說(shuō)一下

編譯器一般都按照芯片的指令集進(jìn)行劃分

i386:

這種指令集對(duì)應(yīng)的是大家以前常用的32位Intel芯片

i386出生的時(shí)候寄存器還很少,不夠用

所以這廝在函數(shù)調(diào)用中的參數(shù)傳遞全靠壓棧

我們以如下函數(shù)調(diào)用為例

int bar(int i0, int i1, int i2, int i3, int i4, int i5, int i6, int i7, int i8, int i9) {return i0+i1+i2+i3+i4+i5+i6+i7+i8+i9;}

這個(gè)函數(shù)有10個(gè)參數(shù)

i386會(huì)從右至左將實(shí)參逐個(gè)壓入ESP

也就是它的棧幀

匯編表現(xiàn)為

push i9
push i8
...
push i1
push i0
call _bar

大家看的所謂語(yǔ)言書(shū)籍的作者當(dāng)年基本都是i386的使用者

這就是大家看到“壓棧順序從右至左”這一說(shuō)法的原因

X86_64:

原來(lái)壓棧方式的調(diào)用約定限制了函數(shù)調(diào)用的速度

因?yàn)閴簵S玫氖莾?nèi)存

這個(gè)時(shí)候世界飛速發(fā)展 摩爾定律潛移默化

芯片很快進(jìn)入了64位時(shí)代

寄存器的數(shù)量大大增加

X86_64開(kāi)始動(dòng)用部分寄存器來(lái)完成參數(shù)傳遞的工作

其中RDI, RSI, RDX, RCX, R8D, R9D寄存器分別用于

正序存儲(chǔ)第1至第6個(gè)實(shí)參

剩下的更多參數(shù)就采用老辦法

逆序壓入棧幀

還是以上文的函數(shù)例子

X86_64的匯編表現(xiàn)為

movq i0, %rdi
movq i1, %rsi
movq i2, %rdx
movq i3, %rcx
movq i4, %r8d
movq i5, %r9d
push i9
push i8
push i7
push i6
callq _bar

由此可見(jiàn)

到了64位

函數(shù)調(diào)用就不再是i386那樣式兒的一概從右至左壓棧了

而是當(dāng)參數(shù)少于6個(gè)時(shí)音诫,直接從左至右使用寄存器

參數(shù)太多時(shí)才會(huì)動(dòng)用堆棧

事實(shí)上這種取舍是非常合理的

因?yàn)榫幋a規(guī)范一般都會(huì)要求大家設(shè)計(jì)函數(shù)調(diào)用不要超過(guò)4個(gè)形參

大家平時(shí)使用的普通函數(shù)大都沒(méi)有或者只有一個(gè)形參

ARM:

ARM指令集的芯片大量用于移動(dòng)終端

大家的手機(jī)芯片大部分都是ARM架構(gòu)的

這里不帶數(shù)字的ARM默認(rèn)是32位的指令集

與Intel的區(qū)別在于ARM已經(jīng)使用寄存器來(lái)完成參數(shù)傳遞了

策略是前4個(gè)參數(shù)使用R0, R1, R2, R3來(lái)傳遞

多出的參數(shù)用堆棧

ARM64:

ARM64和X86_64很像

它使用了X0-X5存儲(chǔ)前6個(gè)參數(shù)惨奕,其他用堆棧

雖然不同指令集的編譯器所使用的匯編語(yǔ)言和寄存器名稱有些許出入

相信大家能夠理解

以上講解正是為了闡明一個(gè)道理

不同編譯器的調(diào)用約定是不同的

一定要樹(shù)立這個(gè)意識(shí)形態(tài)

盡信書(shū)不如無(wú)書(shū)

我以X86_64這個(gè)目前最流行的臺(tái)式計(jì)算機(jī)芯片指令集來(lái)舉例

看看題目中的語(yǔ)句會(huì)被編譯器翻譯成什么樣的匯編代碼

    pushq    %rbp
    movq    %rsp, %rbp
# 第一個(gè)參數(shù)是格式化字符串,即下面的傳給rdi寄存器
    leaq    L_.str(%rip), %rdi
# 以下分別是三次*p++竭钝,分別傳給rsi, rdx, rcx寄存器  movl    $49, %esi
    movl    $50, %edx
    movl    $51, %ecx
    xorl    %eax, %eax
    callq    _printf
    xorl    %eax, %eax
    popq    %rbp
    retq
    .section    __TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
    .asciz    "%c%c%c\n"

由匯編代碼可知梨撞,我們?cè)赬86_64上根本不會(huì)用到調(diào)用棧

因?yàn)閰?shù)數(shù)量尚未超過(guò)6個(gè)

那就不會(huì)有所謂從右向左求值的說(shuō)法

接下來(lái)我們要深入探討調(diào)用約定的問(wèn)題

考察如下代碼

int foo0() {printf("%s\n", __PRETTY_FUNCTION__); return 0;}
int foo1() {printf("%s\n", __PRETTY_FUNCTION__); return 1;}
int foo2() {printf("%s\n", __PRETTY_FUNCTION__); return 2;}
int foo3() {printf("%s\n", __PRETTY_FUNCTION__); return 3;}
int foo4() {printf("%s\n", __PRETTY_FUNCTION__); return 4;}
int foo5() {printf("%s\n", __PRETTY_FUNCTION__); return 5;}
int foo6() {printf("%s\n", __PRETTY_FUNCTION__); return 6;}
int foo7() {printf("%s\n", __PRETTY_FUNCTION__); return 7;}
int foo8() {printf("%s\n", __PRETTY_FUNCTION__); return 8;}
int foo9() {printf("%s\n", __PRETTY_FUNCTION__); return 9;}
int main() {
    printf("%d%d%d%d%d%d%d%d%d%d\n", foo0(), foo1(), foo2(), foo3(), foo4(), foo5(), foo6(), foo7(), foo8(), foo9());
}

大家不妨思考下10個(gè)fooX()函數(shù)最終的執(zhí)行順序如何?

以下是打印出來(lái)的實(shí)驗(yàn)結(jié)果

int foo0()

int foo1()

int foo2()

int foo3()

int foo4()

int foo5()

int foo6()

int foo7()

int foo8()

int foo9()

0123456789

也許這會(huì)出乎一些胖友的意料

畢竟上文描述X86_64先存寄存器再壓棧

那么是否foo6 - foo9應(yīng)該倒序執(zhí)行香罐?

讓我們看看匯編代碼

# 所有參數(shù)表達(dá)式中的函數(shù)調(diào)用已經(jīng)提前完成
    leaq    L___PRETTY_FUNCTION__._Z4foo0v(%rip), %rdi
    callq    _puts
    leaq    L___PRETTY_FUNCTION__._Z4foo1v(%rip), %rdi
    callq    _puts
    leaq    L___PRETTY_FUNCTION__._Z4foo2v(%rip), %rdi
    callq    _puts
    leaq    L___PRETTY_FUNCTION__._Z4foo3v(%rip), %rdi
    callq    _puts
    leaq    L___PRETTY_FUNCTION__._Z4foo4v(%rip), %rdi
    callq    _puts
    leaq    L___PRETTY_FUNCTION__._Z4foo5v(%rip), %rdi
    callq    _puts
    leaq    L___PRETTY_FUNCTION__._Z4foo6v(%rip), %rdi
    callq    _puts
    leaq    L___PRETTY_FUNCTION__._Z4foo7v(%rip), %rdi
    callq    _puts
    leaq    L___PRETTY_FUNCTION__._Z4foo8v(%rip), %rdi
    callq    _puts
    leaq    L___PRETTY_FUNCTION__._Z4foo9v(%rip), %rdi
    callq    _puts
    subq    $8, %rsp
# 編譯器將參數(shù)表達(dá)式的返回值分別傳入相應(yīng)的寄存器或棧幀
    leaq    L_.str.1(%rip), %rdi
    movl    $0, %esi
    movl    $1, %edx
    movl    $2, %ecx
    movl    $3, %r8d
    movl    $4, %r9d
    movl    $0, %eax
    pushq    $9
    pushq    $8
    pushq    $7
    pushq    $6
    pushq    $5
    callq    _printf
    addq    $48, %rsp
    xorl    %eax, %eax
    popq    %rbp
    retq
    .section    __TEXT,__cstring,cstring_literals
L___PRETTY_FUNCTION__._Z4foo0v:         ## @__PRETTY_FUNCTION__._Z4foo0v
    .asciz    "int foo0()"
L___PRETTY_FUNCTION__._Z4foo1v:         ## @__PRETTY_FUNCTION__._Z4foo1v
    .asciz    "int foo1()"
L___PRETTY_FUNCTION__._Z4foo2v:         ## @__PRETTY_FUNCTION__._Z4foo2v
    .asciz    "int foo2()"
L___PRETTY_FUNCTION__._Z4foo3v:         ## @__PRETTY_FUNCTION__._Z4foo3v
    .asciz    "int foo3()"
L___PRETTY_FUNCTION__._Z4foo4v:         ## @__PRETTY_FUNCTION__._Z4foo4v
    .asciz    "int foo4()"
L___PRETTY_FUNCTION__._Z4foo5v:         ## @__PRETTY_FUNCTION__._Z4foo5v
    .asciz    "int foo5()"
L___PRETTY_FUNCTION__._Z4foo6v:         ## @__PRETTY_FUNCTION__._Z4foo6v
    .asciz    "int foo6()"
L___PRETTY_FUNCTION__._Z4foo7v:         ## @__PRETTY_FUNCTION__._Z4foo7v
    .asciz    "int foo7()"
L___PRETTY_FUNCTION__._Z4foo8v:         ## @__PRETTY_FUNCTION__._Z4foo8v
    .asciz    "int foo8()"
L___PRETTY_FUNCTION__._Z4foo9v:         ## @__PRETTY_FUNCTION__._Z4foo9v
    .asciz    "int foo9()"
L_.str.1:                               ## @.str.1
    .asciz    "%d%d%d%d%d%d%d%d%d%d\n"

代碼雖長(zhǎng) 但是邏輯還是比較清晰的

說(shuō)明了一個(gè)道理

參數(shù)中的函數(shù)調(diào)用完全是提前完成的

編譯器可以按照自有的順序來(lái)執(zhí)行

這里X86_64就是按照從左至右的順序執(zhí)行的

并沒(méi)有受 “函數(shù)結(jié)果是存儲(chǔ)于寄存器還是堆椢圆ǎ” 這個(gè)問(wèn)題的影響

也不需要等到傳入各自的寄存器或棧幀前 再忙不迭地執(zhí)行參數(shù)表達(dá)式

綜合上述

我以X86_64做了一些示例

這些例子給大家透露的提示就是

對(duì)于調(diào)用約定 每個(gè)編譯器都可能有其內(nèi)部實(shí)現(xiàn)

一些老舊書(shū)籍所言的函數(shù)調(diào)用參數(shù)傳遞執(zhí)行順序

很可能并未考慮所有編譯器的情況

即使考慮了不同編譯器的情況

然而卻忽略了

參數(shù)中表達(dá)式的執(zhí)行順序其實(shí)與傳參順序無(wú)關(guān) 這個(gè)事實(shí)

愿諸君能夠以嚴(yán)謹(jǐn)?shù)膽B(tài)度 找規(guī)范做實(shí)驗(yàn)去探究和求證所遇到的問(wèn)題

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市庇茫,隨后出現(xiàn)的幾起案子港粱,更是在濱河造成了極大的恐慌,老刑警劉巖旦签,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件查坪,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡宁炫,警方通過(guò)查閱死者的電腦和手機(jī)偿曙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)羔巢,“玉大人望忆,你說(shuō)我怎么就攤上這事《浞祝” “怎么了炭臭?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)袍辞。 經(jīng)常有香客問(wèn)我鞋仍,道長(zhǎng),這世上最難降的妖魔是什么搅吁? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任威创,我火速辦了婚禮,結(jié)果婚禮上谎懦,老公的妹妹穿的比我還像新娘肚豺。我一直安慰自己,他們只是感情好界拦,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布吸申。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪截碴。 梳的紋絲不亂的頭發(fā)上梳侨,一...
    開(kāi)封第一講書(shū)人閱讀 51,365評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音日丹,去河邊找鬼走哺。 笑死,一個(gè)胖子當(dāng)著我的面吹牛哲虾,可吹牛的內(nèi)容都是我干的丙躏。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼束凑,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼晒旅!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起汪诉,我...
    開(kāi)封第一講書(shū)人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤敢朱,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后摩瞎,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體拴签,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年旗们,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蚓哩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡上渴,死狀恐怖岸梨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情稠氮,我是刑警寧澤曹阔,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站隔披,受9級(jí)特大地震影響赃份,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜奢米,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一抓韩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鬓长,春花似錦谒拴、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)炭序。三九已至,卻和暖如春苍日,著一層夾襖步出監(jiān)牢的瞬間少态,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工易遣, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人嫌佑。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓豆茫,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親屋摇。 傳聞我的和親對(duì)象是個(gè)殘疾皇子揩魂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 原文地址:C語(yǔ)言函數(shù)調(diào)用棧(一)C語(yǔ)言函數(shù)調(diào)用棧(二) 0 引言 程序的執(zhí)行過(guò)程可看作連續(xù)的函數(shù)調(diào)用。當(dāng)一個(gè)函數(shù)執(zhí)...
    小豬啊嗚閱讀 4,610評(píng)論 1 19
  • 關(guān)于 C/C++ 函數(shù)調(diào)用約定炮温,大多數(shù)時(shí)候并不會(huì)影響程序邏輯火脉,但遇到跨語(yǔ)言編程時(shí),了解一下還是有好處的柒啤。 VC 中...
    王守偉閱讀 2,321評(píng)論 0 2
  • 函數(shù)調(diào)用約定 在C語(yǔ)言中倦挂,假設(shè)我們有這樣的一個(gè)函數(shù): int function(int a,int b) 調(diào)用時(shí)只...
    羅蓁蓁閱讀 611評(píng)論 0 4
  • 我們都是 我們都是沒(méi)人要的 野孩子 在草地、雪山 自由生長(zhǎng) 我們都是“壞”孩子 我們無(wú)人取代 你屬于孤獨(dú) 我即是寂...
    散夏無(wú)淚閱讀 152評(píng)論 0 2
  • 我無(wú)奈的想放棄尊嚴(yán)的抵抗担巩,給麥子打電話的時(shí)候方援,那個(gè)前凸后翹的前臺(tái)卻扭著個(gè)屁股朝我走來(lái)。 想當(dāng)年涛癌,我剛進(jìn)公司的時(shí)候犯戏,...
    你有一條未讀信息閱讀 481評(píng)論 0 0