返回值不是挺簡單的嗎?有什麼好研究的。
其實返回值不簡單,下面就讓我們來看看返回值有什麼好研究的。
在操作系統中以linux為例),每個程序都需要有一個返回值,返回給操作系統.
在shell中,可以利用echo $?查看程序的返回值
可以看到,not_exist不存在,返回2,main.c存在,返回0,一般返回0表示成功,而返回非0表示失敗或者其他意義。
其實這個返回值是存放在eax中的,c規范要求main必須返回int,而int和eax長度是一的(32位系統)。
這個匯編程序只有一條指令,將4存到eax,檢測返回值發現是4。
如果你的程序用void main(),有的編譯器會報錯,有的會警告,如果編譯過了,運行時一般沒問題。
- int f()
- {
- return 100;
- }
- void main()
- {
- f();
- }
函數f把返回值放到eax了,main函數什麼都沒做,所以返回值還是100。
但是我們來看另外一個例子
//file:haha.c
- struct xxx{
- int a[50];
- };
- struct xxx main()
- {
- struct xxx haha;
- return haha;
- }
為什麼會出現段錯誤?我們後面會研究它。
我們先把返回值進行分類:
首先是基本類型,void,char,short,long,long long,float,double,指針
然後是結構類型struct。
對於void類型,沒有返回值,不做討論。
char只有1個字節,eax有4個字節,怎麼存?只用低8位al就可以了。下面是示例
- //示例1:返回值為char
- /*C代碼*/
- char f()
{
char a = 'a';
return a;
}
int main()
{
char b = f();
return 0;
}
- /*匯編代碼*/
- .file "char.c"
- .text
- .globl f
- f:
- pushl %ebp
- movl %esp, %ebp
- subl $16, %esp
- movb $97, -1(%ebp)
- movsbl -1(%ebp),%eax //符號擴展
- leave
- ret
- .globl main
- main:
- leal 4(%esp), %ecx
- andl $-16, %esp
- pushl -4(%ecx)
- pushl %ebp
- movl %esp, %ebp
- pushl %ecx
- subl $16, %esp
- call f
- movb %al, -5(%ebp)
- movl $0, %eax
- addl $16, %esp
- popl %ecx
- popl %ebp
- leal -4(%ecx), %esp
- ret
從匯編代碼中可以看出,調用完f後,main函數從al中找返回值。
同樣,對於short,int,分別把返回值存放到ax,eax,假如在64位系統裡,那麼long long 返回值是存到rax的,它的長度為64位,在32位系統裡是怎麼存的呢?
在32位系統裡返回64位數,是通過edx和eax聯合實現的,edx存高32位,eax存低32位。
- /*示例2:32位系統上返回64位整數*/
- /*C代碼*/
- long long f()
- {
- long long a = 5;
- return a;
- }
- int main()
- {
- long long b;
- b=f();
- return 0;
- }
- /*匯編代碼*/
- .file "longint.c"
- .text
- .globl f
- f:
- pushl %ebp
- movl %esp, %ebp
- subl $16, %esp
- movl $5, -8(%ebp)
- movl $0, -4(%ebp)
- movl -8(%ebp), %eax
- movl -4(%ebp), %edx
- leave
- ret
- .globl main
- main:
- leal 4(%esp), %ecx
- andl $-16, %esp
- pushl -4(%ecx)
- pushl %ebp
- movl %esp, %ebp
- pushl %ecx
- subl $20, %esp
- call f
- movl %eax, -16(%ebp)
- movl %edx, -12(%ebp)
- movl $0, %eax
- addl $20, %esp
- popl %ecx
- popl %ebp
- leal -4(%ecx), %esp
- ret
對於浮點類型,雖然運算過程中會存放在eax等普通寄存器中,但是作為返回值時,不會用eax,edx等,即使運算結果已經存到了eax中,也要再壓到浮點數寄存器堆棧中,在主調函數中,會認為返回結果存到浮點數寄存器了,當然,如果你要手動優化匯編代碼也是沒問題的。
下面是示例。
- /*示例3:返回值為浮點數*
- /*C代碼*/
- float f()
- {
- return 0.1;
- }
- int main()
- {
- float a = f();
- return 0;
- }
- /*匯編代碼*/
- .file "float.c"
- .text
- .globl f
- f:
- pushl %ebp
- movl %esp, %ebp
- subl $4, %esp
- movl $0x3dcccccd, %eax
- movl %eax, -4(%ebp)
- flds -4(%ebp) //把結果壓到浮點寄存器棧頂
- leave
- ret
- .globl main
- main:
- leal 4(%esp), %ecx
- andl $-16, %esp
- pushl -4(%ecx)
- pushl %ebp
- movl %esp, %ebp
- pushl %ecx
- subl $16, %esp
- call f
- fstps -8(%ebp) //從浮點寄存器棧頂取數
- movl $0, %eax
- addl $16, %esp
- popl %ecx
- popl %ebp
- leal -4(%ecx), %esp
- ret
關於浮點寄存器及浮點運算指令,可參考:http://www.diybl.com/course/3_program/hb/hbjs/2007124/89946.html
如果返回值為指針?那肯定是用eax(32bit)或者rax(64bit)了。不管是什麼類型的指針,都一樣,我們來看一個奇怪的程序。
- /*示例4:返回值為指針*/
- /*C代碼*/
- int f()
- {
- return 5;
- }
- int (*whatisthis()) () //這個函數的返回類型是函數指針
- {
- return f;
- }
- int main()
- {
- int (*a) ();
- int b;
- a = whatisthis();
- b = a();
- printf("%d\n",b);
- return 0;
- }
- /*匯編代碼*/
- .file "ret_fun.c"
- .text
- .globl f
- f:
- pushl %ebp
- movl %esp, %ebp
- movl $5, %eax
- popl %ebp
- ret
- .globl whatisthis
- whatisthis:
- pushl %ebp
- movl %esp, %ebp
- movl $f, %eax
- popl %ebp
- ret
- .LC0:
- .string "%d\n"
- .text
- .globl main
- main:
- leal 4(%esp), %ecx
- andl $-16, %esp
- pushl -4(%ecx)
- pushl %ebp
- movl %esp, %ebp
- pushl %ecx
- subl $36, %esp
- call whatisthis
- movl %eax, -12(%ebp)
- movl -12(%ebp), %eax
- call *%eax
- movl %eax, -8(%ebp)
- movl -8(%ebp), %eax
- movl %eax, 4(%esp)
- movl $.LC0, (%esp)
- call printf
- movl $0, %eax
- addl $36, %esp
- popl %ecx
- popl %ebp
- leal -4(%ecx), %esp
- ret
一個函數的返回值可以是函數指針,定義一個這樣的函數如下:
函數1 int f(int,char)
函數2 返回值為上面函數的類型的指針,假如函數名為g,參數為float
那麼g的定義為 int (* g(float x) ) (int,char)
基本類型討論完了,那麼struct類型呢?struct可大可小,怎麼存到寄存器裡呢?
答案是:主調函數會把被賦值對象的地址傳給被調用函數。你可能會說這不是傳引用嗎,其實傳引用傳值什麼的都是浮雲。
還有一個問題就是,對於struct xxx { char a; };這樣的結構也要傳地址嗎?答案是肯定的,gcc是這樣做的,其它編譯器可能不這樣,當然也可以手動修改匯編代碼。
- /*示例5:struct只有一個字節*/
- /*C代碼*/
- struct xxx{
- char a;
- };
- struct xxx f()
- {
- struct xxx x;
- x.a = '9';
- return x;
- }
- int main()
- {
- struct xxx y = f();
- return 0;
- }
- /*匯編代碼*/
- .file "struct_char.c"
- .text
- .globl f
- f:
- pushl %ebp
- movl %esp, %ebp
- subl $16, %esp
- movl 8(%ebp), %edx //取出地址,放入edx
- movb $57, -1(%ebp)
- movzbl -1(%ebp), %eax //'9'放到 al
- movb %al, (%edx) //將al內容寫到edx指向的地址
- movl %edx, %eax
- leave
- ret $4
- .globl main
- main:
- leal 4(%esp), %ecx
- andl $-16, %esp
- pushl -4(%ecx)
- pushl %ebp
- movl %esp, %ebp
- pushl %ecx
- subl $24, %esp
- leal -21(%ebp), %eax //地址放到eax
- movl %eax, (%esp) //地址壓入棧中
- call f
- subl $4, %esp //沒有取返回值的指令了
- movzbl -21(%ebp), %eax//因為已經寫到目的地址了
- movb %al, -5(%ebp)
- movl $0, %eax
- movl -4(%ebp), %ecx
- leave
- leal -4(%ecx), %esp
- ret
我們再來看個復雜點的例子
- /*示例6: struct較大*/
- /*C代碼*/
- struct xxx {
- char a[10];
- };
- struct xxx f(int a)
- {
- struct xxx t;
- t.a[9] = 1;
- return t;
- }
- int main()
- {
- struct xxx m=f(1);
- return 0;
- }
- /*匯編代碼*/
- .file "struct.c"
- .text
- .globl f
- f:
- pushl %ebp
- movl %esp, %ebp
- subl $16, %esp
- movl 8(%ebp), %edx //取地址
- movb $1, -1(%ebp)
- movl -10(%ebp), %eax
- movl %eax, (%edx)
- movl -6(%ebp), %eax
- movl %eax, 4(%edx)
- movzwl -2(%ebp), %eax
- movw %ax, 8(%edx)
- movl %edx, %eax
- leave
- ret $4
- .globl main
- main:
- leal 4(%esp), %ecx
- andl $-16, %esp
- pushl -4(%ecx)
- pushl %ebp
- movl %esp, %ebp
- pushl %ecx
- subl $24, %esp
- leal -14(%ebp), %eax
- movl $1, 4(%esp) //先壓入參數
- movl %eax, (%esp) //再壓入返回值地址
- call f
- subl $4, %esp
- movl $0, %eax
- movl -4(%ebp), %ecx
- leave
- leal -4(%ecx), %esp
- ret
進入被調用函數後的堆棧情況
它會到假定8(%ebp)處存放著返回值的地址。這也是為什麼main的返回值為struct時會引起段錯誤,main函數認為這個地方存著返回值的地址,實際上這個地方是操作系統寫入的特定值,把這個當作返回值的地址亂寫,肯定會引起段錯誤。
下面這個程序
假如對於struct,有返回值的函數卻不賦值怎麼辦?
比如
- struct xxx {
- char a[10];
- };
- struct xxx f(int a)
- {
- struct xxx t;
- t.a[9] = 1;
- return t;
- }
- int main()
- {
- f(1);
- return 0;
- }
對於上述程序,主調用函數需要開辟垃圾空間作為返回值空間,感興趣的可以驗證下看看。
補充:
gcc支持代碼塊有返回值
比如a = { int b = 2; int c = 3; c-b;} 最終a = 1;
根據我的測試:代碼塊裡必須有除了變量聲明的其他語句,否則不對,不能有return;
另外,只能對基本類型賦值,struct類型不能賦值。
最後的結果是:代碼塊執行結束後,取出eax的值,檢查要賦值的變量類型,如果是char,取al,如果是int,取eax,如果是long long,符號擴展,如果是float或者double,將eax強制轉換成浮點數。
下面代碼可正常運行:
- int main()
- {
- int a;
- long long a1;
- double a2;
- a = {int b = 5; printf("xxx\n");;};
- a1 = {int b = 5;int c = 2; 3-4;b-c;};
- a2 = {int b = 5;int c = 2; 10-8;};
- printf("%d\n",a);
- printf("%ld\n",a1);
- printf("%lf\n",a2);
- return 0;
- }
上面代碼中的3-4會被忽略,因為沒有用,而10-8不會被忽略,因為它在代碼塊最後,但是不是執行sub指令,直接movl $2, %eax;
這東西有用嗎?沒用我就不去研究它了,確實用到了,在Linux內核裡,contain_of這個宏用到了上述內容,所以我稍微研究了下。
維基百科講的比較詳細,http://zh.wikipedia.org/wiki/%E5%9D%97_(C%E8%AF%AD%E8%A8%80%E6%89%A9%E5%B1%95)
本文出自 “牛哥的博客” 博客,請務必保留此出處http://nxlhero.blog.51cto.com/962631/703953