C语言函数参数入栈的理解分析
_stdcall介绍

stdcall调用约定:stdcall很多时候被称为pascal调用约定,因为pascal是早期很常见的一种教学用计算机程序设计语言,其语法严谨,使用的函数调用约定就是stdcall。
在Microsoft C++系列的C/C++编译器中,常常用PASCAL宏来声明这个调用约定,类似的宏还有WINAPI和CALLBACK。
stdcall调用约定声明的语法为(以前文的那个函数为例):int __stdcall function(int a,int b)stdcall的调用约定意味着:1)参数从右向左压入堆栈,2)函数自身修改堆栈 3)函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸。
以上述这个函数为例,参数b首先被压栈,然后是参数a,函数调用function(1,2)调用处翻译成汇编语言将变成:push 2 第二个参数入栈push 1 第一个参数入栈call function 调用参数,注意此时自动把cs:eip入栈而对于函数自身,则可以翻译为:push ebp 保存ebp寄存器,该寄存器将用来保存堆栈的栈顶指针,可以在函数退出时恢复mov ebp,esp 保存堆栈指针mov eax,[ebp + 8H] 堆栈中ebp指向位置之前依次保存有ebp,cs:eip,a,b,ebp +8指向aadd eax,[ebp + 0CH] 堆栈中ebp + 12处保存了bmov esp,ebp 恢复esppop ebpret 8而在编译时,这个函数的名字被翻译成_function@8注意不同编译器会插入自己的汇编代码以提供编译的通用性,但是大体代码如此。
其中在函数开始处保留esp到ebp中,在函数结束恢复是编译器常用的方法。
从函数调用看,2和1依次被push进堆栈,而在函数中又通过相对于ebp(即刚进函数时的堆栈指针)的偏移量存取参数。
函数结束后,ret 8表示清理8个字节的堆栈,函数自己恢复了堆栈。
单片机C语言 必知的数据存储与程序编写知识 附单片机应用编程知识介绍

一、五大内存分区内存分成5个区,它们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
1、栈区(StaCk):FIFo就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。
里面的变量通常是局部变量、函数参数等。
2、堆区(heap):就是那些由new分配的内存块,它们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。
如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
3、自由存储区:就是那些由malloc等分配的内存块,它和堆是十分相似的,不过它是用free 来结束自己的生命。
4、全局/静态存储区:全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
5、常量存储区:这是一块比较特殊的存储区,它们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多)code/data/stack内存主要分为代码段,数据段和堆栈。
代码段放程序代码,属于只读内存。
数据段存放全局变量,静态变量,常量等,堆里存放自己malloc或new出来的变量,其他变量就存放在栈里,堆栈之间空间是有浮动的。
数据段的内存会到程序执行完才释放。
调用函数先找到函数的入口地址,然后计算给函数的形参和临时变量在栈里分配空间,拷贝实参的副本传给形参,然后进行压栈操作,函数执行完再进行弹栈操作。
字符常量一般放在数据段,而且相同的字符常量只会存一份。
二、C语言程序的存储区域1、由C语言代码(文本文件)形成可执行程序(二进制文件),需要经过编译-汇编-连接三个阶段。
编译过程把C语言文本文件生成汇编程序,汇编过程把汇编程序形成二进制机器代码,连接过程则将各个源文件生成的二进制机器代码文件组合成一个文件。
2、C语言编写的程序经过编译-连接后,将形成一个统一文件,它由几个部分组成。
c语言之参数概念

c语言之参数概念C语言作为一种高级编程语言,在软件开发领域中具有广泛的应用。
在C语言中,参数(parameter)是一项重要的概念,用来传递变量或数值给函数。
本文将深入探讨C语言中参数的定义、传递方式以及参数的不同类型。
1. 参数的定义在C语言中,参数是函数的一部分,用于接收传递给函数的值或变量。
它可以是数据类型,例如整型、字符型、浮点型等,也可以是自定义的结构体、指针等。
定义参数的目的是为了在函数内部使用它们进行某些操作。
2. 参数的传递方式C语言中参数的传递方式主要有两种:值传递和引用传递。
2.1 值传递值传递是指将实际参数的值复制给形式参数,函数内部对形式参数的修改不会影响到实际参数的值。
这种传递方式适用于简单数据类型,例如整型、字符型等。
在函数调用过程中,实参的值会在栈上分配内存,然后复制到形参的空间中。
这样,在函数内部对形参的修改只会影响到形参本身,不会影响到实参。
2.2 引用传递引用传递是指将实际参数的地址传递给形式参数,函数内部对形式参数的修改会影响到实际参数的值。
这种传递方式适用于数组、结构体和指针等复杂数据类型。
在函数调用过程中,形参是实参的一种别名,它们共享相同的内存空间。
因此,在函数内部对形参的修改会直接反映在实参上。
3. 参数的类型C语言中的参数类型包括基本数据类型和复合数据类型。
3.1 基本数据类型基本数据类型是C语言中最常见的参数类型,包括整型、字符型和浮点型等。
例如:int add(int a, int b) {return a + b;}上述代码中的add函数有两个整型参数a和b,用于进行加法运算,并返回结果。
3.2 复合数据类型复合数据类型是由多个基本数据类型组合而成的参数类型,包括数组、结构体和指针等。
例如:void bubbleSort(int arr[], int n) {// 冒泡排序算法}上述代码中的bubbleSort函数接受一个整型数组arr和数组长度n作为参数,用于实现冒泡排序算法。
c语言栈实验总结

c语言栈实验总结在实验中,我们使用C语言编写了栈的相关代码,并进行了多个测试和验证。
通过这些实验,我们对栈的基本特征、操作和应用有了更深入的理解。
我们需要明确栈的定义和特点。
栈是一种具有特定限制的线性数据结构,它的特点是“后进先出”(Last In First Out,LIFO)。
这意味着在栈的操作中,最后一个进入栈的元素将首先被访问和操作,而之前的元素则需要等待。
在实验中,我们首先实现了栈的基本操作,包括创建栈、入栈、出栈和判断栈是否为空。
通过这些操作,我们可以有效地管理栈中的元素,并根据需要进行添加和删除。
接下来,我们进行了一系列的测试和验证,以确保栈的操作和功能的正确性。
我们通过不同的测试用例,模拟了各种情况下的栈操作,包括正常情况下的入栈和出栈、栈的空和满状态的判断,以及异常情况下的错误处理。
通过这些测试,我们可以验证栈的实现是否符合预期,并检查代码中是否存在潜在的问题。
在实验过程中,我们还探讨了栈的应用场景和实际用途。
栈的一个典型应用是函数调用过程中的函数调用栈。
当一个函数被调用时,其局部变量和返回地址等信息被压入栈中,当函数执行完毕后,这些信息再从栈中被弹出,使得程序可以正确地返回到原来的调用点。
通过理解函数调用栈的原理和实现,我们可以更好地理解函数调用的工作原理,并能更好地处理函数之间的交互和数据传递。
栈还可以用于解决一些特定的问题,如括号匹配、逆波兰表达式求值等。
通过使用栈,我们可以方便地处理这些问题,并提高程序的效率和可读性。
总结来说,通过C语言栈的实验,我们深入了解了栈的概念、操作和应用,掌握了栈的基本原理和使用方法。
在实验中,我们通过编写代码、进行测试和验证,验证了栈的正确性,并探讨了栈的应用场景和实际用途。
通过这些实验,我们不仅提高了对栈的理解和掌握,还培养了我们的编程能力和问题解决能力。
希望通过这些实验,我们可以更好地应用栈的知识,解决实际问题,并在以后的学习和工作中取得更好的成果。
c++参数入栈顺序总结

总结一下: 编译出来的c/c++程序的参数压栈顺序只和编译器相关!在C语言中,假设我们有这样的一个函数:int function(int a,int b)调用时只要用result = function(1,2)这样的方式就可以使用这个函数。
但是,当高级语言被编译成计算机可以识别的机器码时,有一个问题就凸现出来:在CPU中,计算机没有办法知道一个函数调用需要多少个、什么样的参数,也没有硬件可以保存这些参数。
也就是说,计算机不知道怎么给这个函数传递参数,传递参数的工作必须由函数调用者和函数本身来协调。
为此,计算机提供了一种被称为栈的数据结构来支持参数传递。
栈是一种先进后出的数据结构,栈有一个存储区、一个栈顶指针。
栈顶指针指向堆栈中第一个可用的数据项(被称为栈顶)。
用户可以在栈顶上方向栈中加入数据,这个操作被称为压栈(Push),压栈以后,栈顶自动变成新加入数据项的位置,栈顶指针也随之修改。
用户也可以从堆栈中取走栈顶,称为弹出栈(pop),弹出栈后,栈顶下的一个元素变成栈顶,栈顶指针随之修改。
函数调用时,调用者依次把参数压栈,然后调用函数,函数被调用以后,在堆栈中取得数据,并进行计算。
函数计算结束以后,或者调用者、或者函数本身修改堆栈,使堆栈恢复原装。
在参数传递中,有两个很重要的问题必须得到明确说明:当参数个数多于一个时,按照什么顺序把参数压入堆栈函数调用后,由谁来把堆栈恢复原装在高级语言中,通过函数调用约定来说明这两个问题。
常见的调用约定有:stdcallcdeclthiscallnaked callstdcall调用约定stdcall很多时候被称为pascal调用约定,因为pascal是早期很常见的一种教学用计算机程序设计语言,其语法严谨,使用的函数调用约定就是stdcall。
在Microsoft C++系列的C/C++编译器中,常常用PASCAL宏来声明这个调用约定,类似的宏还有WINAPI和CALLBACK。
c 函数调用栈追踪

c 函数调用栈追踪函数调用栈是计算机程序执行过程中用于管理函数调用和返回的数据结构。
在C语言中,函数调用栈通常是通过栈(stack)来实现的。
当一个函数被调用时,它的局部变量、参数和返回地址等信息会被压入栈中;当函数执行完毕时,这些信息会被弹出栈。
这个过程可以用来实现函数的嵌套调用和返回。
要进行函数调用栈的追踪,可以使用一些调试工具或者手动插入一些打印语句。
下面是一些方法:1. 使用调试器:- GDB (GNU Debugger) 是一个强大的调试器,可以用于跟踪函数调用栈。
-通过在编译时加上`-g` 选项,可以在生成的可执行文件中包含调试信息,使得GDB 能够更好地理解程序的结构。
```bashgcc -g -o my_program my_program.c```-使用GDB 启动程序并进行调试:```bashgdb ./my_program```-在GDB 中可以使用`bt`(backtrace)命令来打印函数调用栈。
2. 手动插入打印语句:-在函数的入口和出口处插入打印语句,打印相关信息。
-例如,可以在每个函数的开头打印函数名,在函数结束时打印返回信息。
```cvoid myFunction() {printf("Entering myFunction\n");// 函数体printf("Exiting myFunction\n");}```这样,当程序执行时,你可以通过观察打印的信息来了解函数调用的顺序和嵌套关系。
请注意,使用调试器是一种更强大和灵活的方法,因为它允许你在运行时动态地查看和修改程序的状态。
手动插入打印语句通常更适用于简单的调试需求。
C语言及ARM中堆栈指针SP设置的理解与总结

C语言及ARM中堆栈指针SP设置的理解与总结1什么是栈百度这么说:栈是一种特殊的线性表,是一种只允许在表的一端进行插入或删除操作的线性表。
表中允许进行插入、删除操作的一端称为栈顶。
表的另一端称为栈底。
栈顶的当前位置是动态的,对栈顶当前位置的标记称为栈顶指针。
当栈中没有数据元素时,称之为空栈。
栈的插入操作通常称为进栈或入栈,栈的删除操作通常称为退栈或出栈。
简易理解:客栈,即临时寄存的地方,计算机中的堆栈主要用来保存临时数据,局部变量和中断/调用子程序程序的返回地址。
程序中栈主要是用来存储函数中的局部变量以及保存寄存器参数的,如果你用了操作系统,栈中还可能存储当前进线程的上下文。
设置栈大小的一个原则是,保证栈不会下溢出到数据空间或程序空间.CPU在运行程序时,会自动的使用堆栈,所以堆栈指针SP就必须要在调用C程序前设定。
CPU的内存RAM空间存放规律一般是分段的,从地址向高地址,依次为:程序段(.text)、BSS段,上面还可能会有堆空间,然后最上面才是堆栈段。
这样安排堆栈,是因为堆栈的特点决定的,堆栈的指针SP初始化一般在堆栈段的高地址,也就是内存的高地址,然后让堆栈指针向下增长(其实就是递减)。
这样做的好处就是堆栈空间远离了其他段,不会跟其他段重叠,造成修改其他段数据,而引起不可预料的后果,还有设置堆栈大小的原则,要保证栈不会下溢出到数据空间或者程序空间。
所谓堆栈溢出,是指堆栈指针SP向下增长到其他段空间,如果栈指针向下增长到其他段空间,称为堆栈溢出。
堆栈溢出会修改其他空间的值,严重情况下可造成死机. 2堆栈指针的设置开始将堆栈指针设置在内部RAM,是因为不是每个板上都有外部RAM,而且外部RAM 的大小也不相同,而且如果是SDRAM,还需要初始化,在内部RAM开始运行的一般是一个小的引导程序,基本上不怎么使用堆栈,因此将堆栈设置在内部RAM,但这也就要去改引导程序不能随意使用大量局部变量。
C语言中可变参数函数实现原理浅谈

C语言中可变参数函数实现原理浅析1、C函数调用的栈结构可变参数函数的实现与函数调用的栈结构密切相关,正常情况下C的函数参数入栈规则为__stdcall, 它是从右到左的,即函数中的最右边的参数最先入栈。
例如,对于函数:void fun(int a, int b, int c){int d;...}其栈结构为0x1ffc-->d0x2000-->a0x2004-->b0x2008-->c对于任何编译器,每个栈单元的大小都是sizeof(int), 而函数的每个参数都至少要占一个栈单元大小,如函数void fun1(char a, int b, double c, short d) 对一个32的系统其栈的结构就是0x1ffc-->a (4字节)(为了字对齐)0x2000-->b (4字节)0x2004-->c (8字节)0x200c-->d (4字节)因此,函数的所有参数是存储在线性连续的栈空间中的,基于这种存储结构,这样就可以从可变参数函数中必须有的第一个普通参数来寻址后续的所有可变参数的类型及其值。
2. C语言通过几个宏来实现变参的寻址根据函数调用的栈结构,标准C语言中,一般在stdarg.h头文件定义了下面的几个宏,用于实现变参的寻址及可变函数的设计,其中有可能不同的商业编译器的发行时实现的具体代码可能不一样,但是原理都是一样的。
//Linux 2.18内核typedef char * va_list;/*Storage alignment properties -- 堆栈按机器字对齐其中acpi_native_int是一个机器字,32位机的定义是:typedef u32 acpi_native_int*/#define _AUPBND (sizeof (acpi_native_int) - 1)#define _ADNBND (sizeof (acpi_native_int) - 1)/* Variable argument list macro definitions -- 变参函数内部实现需要用到的宏*/#define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd)))#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))#define va_arg(ap, T) (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))#define va_end(ap) (void) 0在X86 32位机器中,以上这几个宏的用途主要是:C语言传递参数是与__stdcall相同的,C语言传递参数时是用push指令从右到左将参数逐个压栈,因此C语言里通过栈指针来访问参数。
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
先来看这样一段程序:
view plain copy
print?
1.#include <string.h>
2.#include <stdlib.h>
3.#include <stdio.h>
4.
5.void print1(int a,int b,int c)
6.{
7. printf("%p\n",&a);
8. printf("%p\n",&b);
9. printf("%p\n",&c);
10.}
11.
12.int main(void)
13.{
14. print1(1,2,3);
15. exit(0);
16.}
它的输出是:
[cpp]view plain copy
print?
1.0022FF40
2.0022FF44
3.0022FF48
发现a,b,c的地址是逐渐增大的,差值是4个字节。
这和我所知道的:C函数参数入栈的顺序是从右到左是相匹配的,而且地址的增大值也
与变量所占的字节数相匹配。
不过当把程序稍微做一下修改,如下:
[cpp]view plain copy
print?
1.#include <string.h>
2.
3.
4.
5.char char char
6.
7.
8.
9.
10.
11.
12.int
13.
14.
15.
16.
再观察一下它的输出:
view plain copy
print?
1.0022FF2C
2.0022FF28
3.0022FF24
怎么和上面的效果是相反的!虽然我知道这肯定编译器的一个技巧,不过参数入栈的顺序是从右到左的概念却动摇了。
为了弄清楚其中的道理,必须观察程序生成的中间.s文件,为此,我执行了以下一条命令:
[cpp]view plain copy
print?
1.gcc -S test.c(当前C文件中保存的程序是文章一开始的那个) 在当前目录下生成test.s
文件
使用vim打开test.s文件(只截取主要内容了):
esp是指向栈顶的指针,ebp是用来备份这个指针的。
栈的形状如下:
esp
ebp
|____________________________________________________
栈的最大值栈的最小值
每压入一个参数入栈,就执行 esp = esp - sizoeof(参数)。
不过在esp值变之前,先备份一下ebp = esp,这样不管最后esp指到哪里去了,函数结束时就用这个ebp就能顺利回到调用者了。
view plain copy
print?
1.print1:
2. pushl %ebp//6.先把ebp压栈,保存这个指针
3. movl %esp, %ebp//7.使ebp这个指针保存着esp这个指针指向的地址值
4. subl $8, %esp//8.使esp - 8,也就是说空下8个字节以便实现某个功能
5. leal 8(%ebp), %eax//9.把(ebp + 8)的地址给eax 这个地方为什么要+8 因为这
个函数在经历第5,6步的时候存在着压了两个4字节入栈的操作。
此时+8就指向了实参
1
6. movl %eax, 4(%esp)//10.这个时候就用到第8步空下来的8个字节中的4个了,原
来是保存值,原理就是用C语言写两个数交换值时的那个第三个变量,即缓冲区
7. movl $.LC0, (%esp)<span style="white-space:pre"> </span>//11.把字符
串“%p\n”压栈从第10,11步来看,两个参数的入栈顺序,其实不管顺序了,两个参
数,最右边的在高地址,最左边的在低地址
8. call printf//12.调用函数printf,又是压栈出栈的操作了<span style="white-
space:pre"> </span>到此可以得到8个字节的缓冲区全部用完了
9. leal 12(%ebp), %eax//13.同第9步,此时获取的是实参2的地址
10. movl %eax, 4(%esp)//14.我想说同上
11. movl $.LC0, (%esp)<span style="white-space:pre"> </span>//15.我想说
同上
12. call printf//16.我想说同上
13. leal 16(%ebp), %eax//17.同第9步,此时获取的是实参3的地址
14. movl %eax, 4(%esp)//18.我想说同上
15. movl $.LC0, (%esp)<span style="white-space:pre"> </span>//19.我想说
同上
16. call printf//20.我想说同上。
到了此处我们就知道,printf打印参数的地址,这
个地址是在main函数中压栈时分配的,是什么就是什么,符合参数入栈的顺序是从右到左
这个说法。
17. leave
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.20
30.3
31.2
32.1
33.
3113
34.
35.
36.
好的,这个程序分析完了,再来看有疑问的程序吧:
view plain copy
print?
1.print2:
2. pushl %ebp//5.我想说同上
3. movl %esp, %ebp//6.我想说同上
4. subl $24, %esp//7.这个就不同上了,比上面那个esp - 8大很多吗,不过要记住,
这24个字节是个缓冲区
5. movl 8(%ebp), %eax//8.把实参1放入eax
6. movl 12(%ebp), %edx//9.把实参2放入edx
7. movl 16(%ebp), %ecx//10.把实参3放入ecx
9.
10.
1324
12
11.1
12.1
13.
14.
24204
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
结束了,知道了原因了。
这计算机执行函数的时候不停的压栈出栈,执行这些精细的操作真是太牛了,不过计算机没有情感,它不会评估这个复杂度,只要一条条执行就行了,就像我们抄作文似的,作文
最后好不好不是我们能决定的,而是作文的作者。
我暴露了-_-|||。
谢谢观赏!
此处的汇编语言是AT&T汇编,相关学习资料地址:点击打开链接
本文参考文章地址:点击打开链接。