c/c++堆栈

在论坛常看到有人问关于指针的问题,已经运行时候出错,或者程序崩溃,或者打印数据不对;或者段错误;
所以写篇文章希望对大家有用;
1;首先谈谈段;在intel处理器中逻辑地址;线性地址;物理地址;逻辑地址就是段地址+段内偏移量;在早期的8086中;逻辑地址是16位段地址×16+段内偏移量;到了后来;intel引入了所谓的实模式和保护模式;所谓实模式也是就兼容早期的8086;段地址依然是段寄存器内容;逻辑地址依然是段地址×16+段内偏移量;所谓保护模式是对于逻辑地址另外一种算法;
线性地址就是我们用段寄存器内容×16+段内偏移量算出的地址;很显然早期的地址只有20位;一种到引入MMU;就有了物理地址的和线性地址的区别;如果没有硬件MMU;那么线性地址就是时间的物理内存的地址;但是引入了MMU;这个线性地址将被MMU控制器重新解析;然后解析到不同的物理地址;这个就是所谓的分页技术;
2;谈到寄存器,我们不妨列出所有的寄存器EAX;EBX;ECX;EDX;ESP;EBP;EDI;ESI;这个8个叫通用寄存器;EIP;EFLAGS;是特殊寄存器;还有CS;DS;ES;SS;FS;GS这些段寄存器;这个里面的内容将作为段的首地址;8086的寄存器没有E;也就是说是AX;BX等;
3;指针;我们知道指针表示地址;我们也常说32bit的系统;指针长度是4字节的;那么这个4字节的内容最后到达写到哪里去呢?毫无疑问;这个指针的内容会被分为段地址和段内偏移量2个部分;段地址部分被存放到段寄存器中;(当然了保护模式下,寄存器的解析是不同于是模式的)在进一步解释指针之前;以及野指针问题;我们来看看保护模式;
4;保护模式;在现在的操作系统中都是开始用实模式;后来用保护模式;正是由于保护模式的存在;才让编程变得容易;
实模式有个缺点就是只能寻1M空间;也就是20位;对于保护模式;有一个8字节的段描述符;这个描述符描述你一个段的基本信息;包括段的起始地址;段的大小;段的颗粒大小;这里有必要说说段的大小和颗粒大小的问题;其实段的大小只是说段从起始地址开始;有多少个颗粒大小;颗粒可以是1byte;也可以是4kbyte;段就是只得颗粒的数量;段大小是20位的;如果颗粒是1byte那么段的最大就是1M;如果颗粒是4k;那么最大就是4G;
好了;那么我们一共有多少个这样的段描述符呢;换句话说我们有多少个段可以使用呢?这个时候16bit的段寄存器不再像实模式那样解释了;16bit的前13bit作为段选择符;后面3bit作为这个段的描述;这里我们不得不说GDT;LDT;GDT;和LDT;是2张表;这个2张表放着段描述符的地址;这个表有13^2个表项;好了现在我们知道所有的寄存器都有13bit的选择子;指示了选择GDT或者LDT表中段描述符的序号;比如说CS=0008H;那么我们知道;前13bit值是1;后面3bit是000;那么他将选择第2个标识符;那么GDT;LDT是什么呢?他们叫全局描述符和局部描述符;一个CPU只有1个GDT;而每个应用程序都可以有一个LDT;GDT;LTD的起始地址存放在寄存器GDTR;LDTD中;

5;说完了intel的段;我们再来看看分页机制;在linux中;并没有用到13^2个段;4个主要的段是内核的代码段;内核的数据段;应用程序代码段;应用程序数据段;他们都设置了段描述符使得其实地址是00000000H;结束地址是FFFFFFFFH;开始我们说段寄存器最后3位用作其他作用;其中一个就是设置CPU访问权限;2bit一个用4中权限;linux只用了0和3;也就是我们平时说的内核态和用户态;对于intel处理器来说如果CPU的寄存器cr0到cr4;里面有说明CPU访问权限;(我没有具体研究过)当CPU访问权限小于段的访问权限那么段是无法被访问的;也就是说CRX中如果说明CPU权限是3;而段权限是0;那么CPU是无法访问这个段的;


6;对于线性地址;处理器通过分页机制来转换到实际的物理地址;一个线性地址;被分成页目录;页表项和偏移量;所以一个页未必存放在内存中;有寄存器标志标识;所以以前有人说一个文件很大是否要申请很大的内存来存放;实际是没有必要的;因为即使你申请了大的内存;操作系统也只有在需要的时候才会给你实际的页;一个页通常是4kb的;这个时候才是真正的分配了物理内存;所以有文章说;如果你申请的数据;或者全局变量很大;可能导致程序过慢;因为你的数据分别放在2个页上;而当系统负载很高;就会导致页面不断和磁盘交互;导致程序运行过慢;这里也说明了一个野指针为什么能够通过编译;但是运行会出错;因为你的指针指示到了本来不属于这个进程的页面;而操作系统这个时候就认为你进行了非法操作;
7;linux典型的程序图;
其实这个图有很多人写过了;但是我还是想copy下;
命令行参数和环境变量


未初始化的数据
初始化的数据
正文
保留区
这里我们可以看出;所谓的正文区就是我们说的代码区;也就是指令;初始化的数据;包括初始化的全局变量;static 修饰的局部变量;未初始化区;也就是未初始化的全局变量;也就是我们所说的BSS区;堆也就是用malloc分配的;所谓的动态内存;所以这个是要释放的;栈也就是我们的自动变量(应该叫这个吧)就是那些局部的变量;命令行参数也就是我们在shell里面输入的参数;
我们每次调用函数;就会吧入口参数;也就是实际参数放在堆中;包括这个函数声明的变量;比如说
int main(int argc,char **argv)
{
   int a = 9;
   f(a);
   b=5;

}
int f(int a)
{
    int b = 6;
}
虽然我们看到int a = 9;和f(a);但是他们并不是同一个变量;我们把前一个叫a1后面一个叫a2;
在堆里面是这样分配的
a1;
a2;
b;
这个就是所谓的传值;因为c语言没有传引;C++的引用我的理解是传引;这里看到main对b赋值;这个是不能够通过的;因为当f返回堆里面的a2和b就不存在了;换句话说堆栈寄存器的偏移量已经自动减少了;论坛有文章说Windows默认的栈是1M的;但是linux我没有看到文章说对栈有大小限制;
好了;如果a是指针呢?是传值吗?还是传引?C语言只有传值换句话说即使是指针也是2个变量;所以我们常常听到有人抱怨说为什么我的输出出错了;
7;对于初学者来说;不太理解指针和指针指向的东西;因为我刚开始的时候也很迷惑;
int a = 9;
int *p =&a;
这个时候你的栈中间有个变量a;有个变量p;只不过变量p的内容是a的地址;所以有人常常
int *p;
&p= 4;
问这个为什么是错的;你的p内是一个垃圾数据;他指向了一个或者在堆;或者在栈;或者在代码区;所以这样的指针必然导致灾难的;
我们常常说栈的变量是自动释放的;而堆是不可以的必须free;我们来看下C语言对于的汇编代码;
#include <stdio.h>
#include <stdlib.h>
int f(int a,int b);
int main(int argc,char**argvs)
{
int *p = malloc (100);
int a = 5;
int b = 6;
int c = f(a,b);
free(p);
}
int f(int a,int b)
{
int c = a+b;
return c;
}
汇编代码
.file "main.c"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $32, %esp
movl $100, (%esp)
call malloc
movl %eax, 28(%esp)
movl $5, 24(%esp) //a 
movl $6, 20(%esp) //b 我们看到栈是向下生长的
movl 20(%esp), %eax
movl %eax, 4(%esp)//将a放到堆栈里面
movl 24(%esp), %eax
movl %eax, (%esp)//将b放到堆栈里面;这里我们明显看到C语言的传值特性;
call f   //调用f
movl %eax, 16(%esp)
movl 28(%esp), %eax
movl %eax, (%esp)
call free
leave
ret
.size main, .-main
.globl f
.type f, @function
f:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl 12(%ebp), %eax
movl 8(%ebp), %edx
leal (%edx,%eax), %eax
movl %eax, -4(%ebp)
movl -4(%ebp), %eax
leave
ret
.size f, .-f
.ident "GCC: (Ubuntu 4.4.1-4ubuntu8) 4.4.1"
.section .note.GNU-stack,"",@progbits

8;想正确使用指针;必须正确理解指针;指针的声明一句话讲完了;从变量名开始;看优先级;然后读指针含义
int (*(*f)(int,int(*)(int,float)))(int);
从f开始
1;*f//f是一个指针
2;(*f)()//f是一个函数指针
3;(*f)(int,int(*)(int,float)))//这个函数指针有2个参数int 和int(*)(int,float);后面一个依然是函数指针;
4;函数f的返回值是什么;(*(*f)(int,int(*)(int,float)))//是指针;
5;这个指针指向什么(*(*f)(int,int(*)(int,float)))(int)//一个参数为int的函数;
6;这个返回的函数指针的返回值是什么int;
所以整体是说;我声明了一个指向2有个参数,返回值是函数指针的函数指针;而2个参数是int和执行函数的指针;
知道怎么读指针了;我们再来看看多维数组
int a[3][4];
这个C语言到底表达了什么意思?有人说3×4的数组;其实这个是不准确的;应该是3个向量;每个向量有4个int元素的数组;
现在我们看看这样一段代码
      int a[3][4];
      int **p;
      p = a;
//这个代码是有警告的;说指针类型不匹配;
那么我们看看这样的情况
      int a[3][4];
     int (*p)[4];
     p = a;
//这样的代码是没有警告的;因为类型匹配了;所以我们可以看出;p实际是指向了一个4个元素的数组;所以多维数组其实是一维数组的向量;
9;数组名字到底是什么;为什么有那么多错误和数组名字有关;
      int a[4];
      a++;
      int *p = a;
      p++;
//大家知道这段代码a++是不能够通过编译的;而p++却可以;所以有很多人问这样的问题;他们都是地址;为什么不可以;
首先在声明a[4]时候;你的栈空间有4个int的空间分别给了 a[0],a[1],a[2],a[3];声明int *p;有一个空间给了p;那么你做p++;这个时候p这个空间是能够存放++之后的值的;(当然这个值未必立刻存放到这个内存中,可能暂时存放在EAX;EBX等寄存器中);而数组名a是什么;没有空间存放a;换句话说;a这个东西对于C的源程序是有用的;他告诉编译器程序员将a[0]的地址用a来表示;但是不给a分配任何空间;所以不难明白为什么对于a++是非法的了;
好了;下面我们来谈谈多维数组的名字问题;
int a[3][4];
我们不难明白a表示a[0]的地址;而a[0]是一个有4个元素的向量;那么a[0]也就是表示这个向量的数组名;因此也是一个地址;同样的道理;a[0]是不允许做++运算的;我们不能明白*a[0];表是a[0][0]的值;
而&a[0];表示a的值;但是由于a[0]和a的表示同一个值;所以我不难明白a[0]=a;至于其他情况;大家应该能够分析;
 
9;关于main函数
C99有这样一段描述main函数的
5.1.2.2.1 Program startup
1 The function called at program startup is named main. The implementation declares no
prototype for this function. It shall be defined with a return type of int and with no
parameters:
int main(void) { /* ... */ }
or with two parameters (referred to here as argc and argv, though any names may be
used, as they are local to the function in which they are declared):
int main(int argc, char *argv[]) { /* ... */ }
or equivalent;9) or in some other implementation-defined manner.
2 If they are declared, the parameters to the main function shall obey the following
constraints:
— The value of argc shall be nonnegative.
— argv[argc] shall be a null pointer.
— If the value of argc is greater than zero, the array members argv[0] through
argv[argc-1] inclusive shall contain pointers to strings, which are given
implementation-defined values by the host environment prior to program startup. The
intent is to supply to the program information determined prior to program startup
from elsewhere in the hosted environment. If the host environment is not capable of
supplying strings with letters in both uppercase and lowercase, the implementation
shall ensure that the strings are received in lowercase.
— If the value of argc is greater than zero, the string pointed to by argv[0]
represents the program name; argv[0][0] shall be the null character if the
program name is not available from the host environment. If the value of argc is
greater than one, the strings pointed to by argv[1] through argv[argc-1]
represent the program parameters.
— The parameters argc and argv and the strings pointed to by the argv array shall
be modifiable by the program, and retain their last-stored values between program
startup and program termination.
这个是C99标准规定;他说明了main函数是没有原型的;他声明方法;int main(void);int main(int argc,char argv[])或者其他;同时标准规定argv[0]必须是程序名字;argv[argc]是NULL指针;其他是程序的参数;都是指向char的指针;并要求程序是能够修改这些参数的;这些参数的生命周期是从程序开始到程序结束;
所以不要再为main函数的方式迷惑了;同时C99规程当到达};或者return ;或者exit()函数;都是main函数返回给操作系统的值;实际上;当main函数结束都会调用exit函数来执行一些操作;比如说关闭文件描述符;把缓冲的数据写到磁盘上等等;同时我们看汇编知道返回值是通过eax返回给OS的;
10;关于有符号和无符号数;
对于大多数人来说无符号以为这不可能小于0;比如说unsigned int a = -9;是不合法的
请看下面的代码
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char **args)
{
signed int a = -9 ;
signed int c = 8;
unsigned int b =  7;
unsigned int d = -10;
printf("%d,%d,%d,%d",a,b,c,d);
return 3;
}
汇编代码
.file "main.c"
.section .rodata
.LC0:
.string "%d,%d,%d,%d"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $48, %esp
movl $-9, 44(%esp)//a
movl $8, 40(%esp)//c
movl $7, 36(%esp)//b
movl $-10, 32(%esp)//d
movl $.LC0, %eax
movl 32(%esp), %edx
movl %edx, 16(%esp)
movl 40(%esp), %edx
movl %edx, 12(%esp)
movl 36(%esp), %edx
movl %edx, 8(%esp)
movl 44(%esp), %edx
movl %edx, 4(%esp)
movl %eax, (%esp)
call printf
movl $3, %eax
leave
ret
.size main, .-main
.ident "GCC: (Ubuntu 4.4.1-4ubuntu8) 4.4.1"
.section .note.GNU-stack,"",@progbits
事实上无符号数并没有和有符号数有多少差别;而且运行的结果也是正确的
那么机器里面究竟是如何区别有符号还是无符号的呢?
我们知道计算机的运算电路是很昂贵的;所以计算机只有加法电路;没有减法电路;减法电路是用加法电路来实现的比如说3-4;其实是做3+(-4);这个时候-4用补码来表示;通常是2的补码;也就是取反加1;对于机器来说他并不知道你给他的数字是补码还是原码;或者说这个码字到底表示什么只在于程序员怎么看待;上面的文章中间得到EFLAGS中有一个bit位S表示运算结果是否有符号;S=1表示最高位是有符号的;S=0表示最高位是无符号的;他只是表示运算的结果;并没有说明相加的数是否带符号;
这个S位的描述是这样的:符号标志存放算术或者逻辑运算之后的结果的算术符号;
int main(int argc,char** argv)
{
    char a = 222;
    char b= 222;
     printf("%d",a+b);
  system("PAUSE");
  return 0;
}
这个代码运行的结果是-68;首先char 是8bit的;222也就是0xDE;a+b=444;这个时候由于数据溢出导致S=1
而结果以256位模得到444-256 = 188;这个时候符号为是1;所以printf认为这个是一个负数;这个负数的补码是188;转换成本来的值就是-68;
我查看了下C99对于unsigned 的说法;并没有找到对unsigned int必须大于0的说法;(如果那个高人找到请通知下);
所以对于有符号或者无符号数;其实是告诉编译器程序员的意图;而机器本身是不知道的;
再看下面的代码
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char **args)
{
signed char a = -9 ;
signed char c = 8;
unsigned char b =  7;
unsigned char d = -10;
printf("%d,%d,%d,%d,%d",a,b,c,d,a+b);
return 3;
}
这里的输出结果有错误 ;
-9,7,8,246,-2
显然d错了;但是也不错;因为对于-10来说他是用246来表示的;我们再看汇编代码;
.file "main.c"
.section .rodata
.LC0:
.string "%d,%d,%d,%d,%d"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
pushl %edi
pushl %esi
pushl %ebx
subl $52, %esp
movb $-9, 47(%esp)//a
movb $8, 46(%esp)//c
movb $7, 45(%esp)//b
movb $-10, 44(%esp)//d
movsbl 47(%esp),%edx
movzbl 45(%esp), %eax
leal (%edx,%eax), %edi
movzbl 44(%esp), %esi//d
movsbl 46(%esp),%ebx
movzbl 45(%esp), %ecx
movsbl 47(%esp),%edx
movl $.LC0, %eax
movl %edi, 20(%esp)
movl %esi, 16(%esp)//d
movl %ebx, 12(%esp)
movl %ecx, 8(%esp)
movl %edx, 4(%esp)
movl %eax, (%esp)
call printf
movl $3, %eax
addl $52, %esp
popl %ebx
popl %esi
popl %edi
movl %ebp, %esp
popl %ebp
ret
.size main, .-main
.ident "GCC: (Ubuntu 4.4.1-4ubuntu8) 4.4.1"
.section .note.GNU-stack,"",@progbits
我们可以看出来;由于S标志位只对算术和逻辑起作用;而对MOV指令不起作用;
(我还是想知道C99到底有没有对unsigned作出限制,请知道的兄弟跟贴教我下)


11;关于标识符连接性的C99解读;
An identifier declared in different scopes or in the same scope more than once can be
made to refer to the same object or function by a process called linkage.21) There are
three kinds of linkage: external, internal, and none.
这个C99的描述;标识符的连接性其实是为了让具有文件作用域的标识符能够作用与其他文件;一个用extern 说明的标识符;其实只是一个声明;他声明了这个标识符;如果外面有这样的标识符的作用范围作用到该标识符的代码块;那么这个标识符和刚才的标识符表示同一个;但是这个声明并不改变原来定义的标识符的链接性;
所以有贴子说在一个h文件定义了一个变量;然后有2个c文件include了这个h文件;导致编译出错;这个问题的解释是这样的;include相当于把h文件的内容直接写到c文件中;这样其实你就定义了2个相同名字的变量;当连接器链接的时候;他无法知道代码是用哪个变量;所以会报重复定义;
大家看3段代码;第1段我用Windows下的CODEBLOCK编译的;编译器选择的是GNU gcc
第2段和第一段一样;不过我是在linux 下用gcc编译的;第3段C代码和第2段有些不同;
//第1;第2段代码;他们相同;
#include <stdio.h>
#include <stdlib.h>

void f(void);
void g(void);
int main(int argc,char *argv[])
{
    g();
    f();
    return 0;
}
void f(void)
{
extern int a;
    a++;
    printf("%d\n",a);
    int a = 100;
extern int a ;
a++;
    printf("%d\n",a);
}
void g(void)
{
extern int a;
printf("%d\n",a);
}
int a = 1;

//上面的代码能够在Windows下编译通过;输出是;1 ,2,101;
而在Linux下;编译出错
main.c: In function ‘f’:
main.c:17: error: declaration of ‘a’ with no linkage follows extern declaration
main.c:14: note: previous declaration of ‘a’ was here
main.c:18: error: extern declaration of ‘a’ follows declaration with no linkage
main.c:17: note: previous definition of ‘a’ was here
第三段代码;我们把最后一句放到include后的第一句
如下
main.c: In function ‘f’:
main.c:17: error: redefinition of ‘a’
main.c:3: note: previous definition of ‘a’ was here
main.c:18: error: extern declaration of ‘a’ follows declaration with no linkage
main.c:17: note: previous definition of ‘a’ was here
虽然这样的代码我们是不可能写的;我故意写出来和大家一起探讨下连接性的问题;对于第一个情况好像是正确的;其实不然;如果我们调换下f和g的调用顺序;我们会发现问题所在;
在代码中我们有  int a = 100;
extern int a ;
对于extern int a ;这个;其实我们是声明了一个a;根据C99标准;这个时候由于int a 的声明导致了全局变量a不可见;那么a就表示一个外部的值那么这个a是多少呢;我们不清楚;肯定不会是全局变量的那个a;因为这个a不可见;所以我们的编译器把那个局部变量作为了具有外部连接性;
C99这样说
If no prior declaration is visible, or if the prior
declaration specifies no linkage, then the identifier has external linkage

这个说法有点问题;究竟是说用extern修饰的a具有外部连接性;还是说int a这个本来没有链接性的;具有了外部链接性呢?linx下的gcc认为是后者;
main.c:17: error: declaration of ‘a’ with no linkage follows extern declaration
main.c:14: note: previous declaration of ‘a’ was here
main.c:18: error: extern declaration of ‘a’ follows declaration with no linkage
main.c:17: note: previous definition of ‘a’ was here
这里第一句说int a这个变量在extern之后;编译器认为在此之前extern修饰的a表示一个外部变量;而声明int a却作用不到这个位置;第3句;是说既然声明的int a 屏蔽了其他a的作用;这个时候说明a的连接性之能够说明int a ;所以说错误的;
而第3中情况;我们由于将全局变量的a 放到了开始;
main.c:17: error: redefinition of ‘a’
main.c:3: note: previous definition of ‘a’ was here
main.c:18: error: extern declaration of ‘a’ follows declaration with no linkage
main.c:17: note: previous definition of ‘a’ was here

所以他报了a定义重复;
我想由于具备变量被声明为外部链接性;这个就和声明了2个相同名字的全局变量一样;会导致很诡异的错误;
而在开始我说如果Windows下将外部变量声明到第一句;输出结果是100;101;101;
嘿嘿;
所以规范代码是很有必要滴;


12;void类型的指针的探讨
我们知道没有void类型的变量;但是有void类型的指针变量;而void指针表示他能够转换成任何一种类型;
所以我们常常这样写
float *fp = NULL;
fp = (float *)calloc(ziseof(float),100);
其实这个是完全没有必要的;
甚至;我们这样的代码也能够通过编译
char **chp;
void *p;
p = chp;
另外;我们知道因为局部变量存放在栈空间;所以我们无法将函数内部的变量返还给调用它的函数;尤其是我们在函数内部用malloc申请了内存;如果处理不当;就会导致内存泄露;
所以我们常常这样声明函数
int f(char **p)
{
    *p = malloc(100);
    return 0;
}
这样在调用f的地方
char *p;
f(&p)
free(p);
这样我们就不会造成内存泄露;
现在我们提出这样一个问题;如果我想写一个函数;但是我事先并不知道申请的内存到底是给什么样的变量的;那么我们就必须用void指针
void f(void *p);
void g(void **p);
由于void是表示任意类型的指针;所以我们用f的方式也是能够表示char **chp;的;那是不是我们用f的形式表示呢
例如
void f(void *p)
{
    *p = malloc(100);
}
而像这样调用
char *p;
f(&p)
free(p);
事实是不可以的;编译器认为*p表示了一个void类型的变量;而不是说void *类型的变量;即使f(&p)是合法的;我们也是无法通过编译的
所以我们不得不用void **这样的形式;换句话说我们必须用指向void *类型的指针;
这次就这么多


13;关于函数返回值的问题;
很多文章提到函数返回值是放到一个临时变量;然后呢再把他赋给某个值;下面我们来看这样一段代码
#include <stdio.h>
#include <stdlib.h>
int a = 1;
int f(int a);
int g(int b);
int main(int argc,char *argv[])

    int a = 10;
    int b = 11;
    a = f(a);
    b = g(b);
    printf("%d\n",a+b);
    return 0;
}
int f(int a)
{
    return a+2;
}
int g(int b)
{
     return b+10;
}
对应的汇编如下
.file "main.c"
.globl a
.data
.align 4
.type a, @object
.size a, 4
a:
.long 1
.section .rodata
.LC0:
.string "%d\n"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $32, %esp
movl $10, 28(%esp) //a栈中位置
movl $11, 24(%esp)//b
movl 28(%esp), %eax
movl %eax, (%esp)
call f
movl %eax, 28(%esp)//赋给a
movl 24(%esp), %eax
movl %eax, (%esp)
call g
movl %eax, 24(%esp)//赋给b
movl 24(%esp), %eax
movl 28(%esp), %edx
addl %eax, %edx
movl $.LC0, %eax
movl %edx, 4(%esp)
movl %eax, (%esp)
call printf
movl $0, %eax
leave
ret
.size main, .-main
.globl f
.type f, @function
f:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
addl $2, %eax//返回值放到了eax
popl %ebp
ret
.size f, .-f
.globl g
.type g, @function
g:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
addl $10, %eax// 返回值放到了eax里面
popl %ebp
ret
.size g, .-g
.ident "GCC: (Ubuntu 4.4.1-4ubuntu8) 4.4.1"
.section .note.GNU-stack,"",@progbits
由此可见一个函数的返回值是尽可能放到寄存器里面;而不会直接放到内存中;事实上我们知道intel处理器是不能够在内存和内存之间直接传递数据的;
所以用中间变量的说法有待商榷;就我个人对临时变量的理解是存放在内存中的;如果硬把寄存器当中存放临时变量;那么这样的说法也是可以的;
所以我们现在又个疑问;如果返回值是一个结构体而结构体很大;那么返回值会怎么做呢?
我没有去试验过;我想编译器应该会优先利用寄存器;如果寄存器不够;那么才会用到存储器;C与指针最后的对寄存器的试验;我们可以知道编译器优先利用寄存器;但是未必用完寄存器再用内存;

对;我说错了;
应该先
a1;
b;
a2;
被调用函数的局部变量时先放入栈的;而实参是后入栈的;sorry

比如函数的返回值,4个字节的eax寄存器传,8个字节的eax,edx一起用,超过了就用堆栈了。
有一点比较有意思的就是,vc下如果返回的结构体比较小就4个字节4个字节mov,如果比较大了的话,他会调用memcpy()把数据拷到栈里...

而且得更正一下,应该说是 堆栈 或者 栈,怎么说是堆了呢 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值