一直对函数调用的具体汇编指令和各种变量在内存的具体分配,一知半解。各种资料都很详细,但是不实践,不亲自查看下内存总不能笃定。那就自己做下。
两个目的:
一,函数和函数调用编译后的汇编指令基本样貌
二,各种变量类型的内存状况。
一 函数和函数调用编译后的汇编指令基本样貌
1),空主函数
2),主函数调用,无返回直,无参函数.
3),主函数调用,无返回直,有参函数
3),主函数调用,有返回直,有参函数.
4) ,被调函数再调用函数.
二,各种变量类型的内存状况。
1).尝试 各种变量在全局或局部,或参数传递的情况.
2)常见语法的编译结果.
1),空主函数
代码:
int HariMain(void)
{
return 0;
}
编译list:
7 [SECTION .text]
8 00000000 GLOBAL _HariMain
9 00000000 _HariMain:
10 00000000 55 PUSH EBP
11 00000001 31 C0 XOR EAX,EAX
12 00000003 89 E5 MOV EBP,ESP
13 00000005 5D POP EBP
14 00000006 C3 RET
Debug下观察寄存器和内存情况
无调用和数据。不需debug.
结论:c的空函数 最基本会有3条指令。
PUSH EBP
MOV EBP,ESP
POP EBP
2),主函数调用,无返回直,无参函数
代码:
int HariMain(void)
{
count();
return 0;
}
void count()
{
int a=1+2;
}
编译list:
7 [SECTION .text]
8 00000000 GLOBAL _HariMain
9 00000000 _HariMain:
10 00000000 55 PUSH EBP
11 00000001 89 E5 MOV EBP,ESP
12 00000003 E8 00000004 CALL _count
13 00000008 5D POP EBP
14 00000009 31 C0 XOR EAX,EAX
15 0000000B C3 RET
16 0000000C GLOBAL _count
17 0000000C _count:
18 0000000C 55 PUSH EBP
19 0000000D 89 E5 MOV EBP,ESP
20 0000000F 5D POP EBP
21 00000010 C3 RET
反汇编代码区(图1)
代码执行前寄存器直
代码执行前堆栈情况.
根据汇编指令大概理解和实验观察点:
1) 主函数执行call之后查看栈
发现 已经有2条4字节数据。根据 图1代码,
Push ebp :压入 ebp 0x00000000
Call .+4 效果如:push eip jmp near ptr 标号
所以栈顶的数据就是 被调函数返回时的指令地址 0x0000002c
同时ip ,指令地址变为 被调函数地址。
Call 指令 是用偏移 数字来表示 指令地址 。这里是.+4.
2)被调函数执行ret 之后 查看栈
执行ret指令,cpu会自动执行效果同样的1条指令。
Pop eip <eip=adr(ss,esp);esp=esp+4>
也就是指令地址重回 调用函数call 之后的下一指令的地址。同时栈顶地址向高地址移动。
确实如此。Ip 已经是0x0000002c ,也就是call 之后的下一个指令地址。
结论:无参,无返回直。
依靠 成对的call ret 指令,来调用函数和返回。
Call : push eip ,
jmp near ptr 标号
把call 之后的指令压栈。再jmp 到 被调函数的内存地址。
Ret : pop eip
返回调用者。
3),主函数调用 无返回直,有参数函数
代码:
void count(int a,int b);
int HariMain(void)
{
count(1,2);
return 0;
}
void count(int a,int b)
{
int c=a+b;
}
编译list:
7 [SECTION .text]
8 00000000 GLOBAL _HariMain
9 00000000 _HariMain:
10 00000000 55 PUSH EBP
11 00000001 89 E5 MOV EBP,ESP
12 00000003 6A 02 PUSH 2
13 00000005 6A 01 PUSH 1
14 00000007 E8 00000004 CALL _count
15 0000000C 31 C0 XOR EAX,EAX
16 0000000E C9 LEAVE
17 0000000F C3 RET
18 00000010 GLOBAL _count
19 00000010 _count:
20 00000010 55 PUSH EBP
21 00000011 89 E5 MOV EBP,ESP
22 00000013 5D POP EBP
23 00000014 C3 RET RET
反编译代码区内存数据:
根据汇编指令大概理解和预测实验点:
1)带参,调用者会把参数入栈. 查看栈数据
调用前 参数确实入栈
执行call,堆栈如之前实验, 继续push eip
2)被调者如何使用参数
因为函数无返回直,并且函数计算的结果,并没有在任何地方使用。编译器直接机智的忽视掉被调函数的所有代码的编译。
结论:确实如 汇编语言 书上所讲。参数入栈是最后的参数先入栈。
4),主函数调用,有返回直,有参函数.
代码:
int count(int a,int b);
int HariMain(void)
{
volatile int sum =count(1,2);
return 0;
}
int count(int a,int b)
{
int c=a+b;
return c;
}
编译list:
7 [SECTION .text]
8 00000000 GLOBAL _HariMain
9 00000000 _HariMain:
10 00000000 55 PUSH EBP
11 00000001 89 E5 MOV EBP,ESP
12 00000003 50 PUSH EAX
13 00000004 6A 02 PUSH 2
14 00000006 6A 01 PUSH 1
15 00000008 E8 00000007 CALL _count
16 0000000D 89 45 FC MOV DWORD [-4+EBP],EAX
17 00000010 31 C0 XOR EAX,EAX
18 00000012 C9 LEAVE
19 00000013 C3 RET
20 00000014 GLOBAL _count
21 00000014 _count:
22 00000014 55 PUSH EBP
23 00000015 89 E5 MOV EBP,ESP
24 00000017 8B 45 0C MOV EAX,DWORD [12+EBP]
25 0000001A 03 45 08 ADD EAX,DWORD [8+EBP]
26 0000001D 5D POP EBP
27 0000001E C3 RET
1)上次实验只验证了参数入栈的情况。这次要看被调者如何使用参数。
使用 MOV EAX,DWORD [12+EBP]
Add EAX,DWORD [8+EBP]
来取得实参。
为什么是这样。直接看图1。因为mov ebp esp.
Ebp 的直为0x30ffe8. 也就是指向当时的栈顶。
再看图二DWORD [12+EBP] 就是参数2。
2)被调者如何取得返回值
MOV DWORD [-4+EBP],EAX
从寄存器eax 获得返回值,并给预先空处的栈的某个位置赋直(实参的后面地址)
执行 MOV DWORD [-4+EBP],EAX .后的寄存器和堆栈 情况。
结论:
C 编译器,被调函数一般会把返回值先放到寄存器eax 中。
调用函数需要返回直时,又会从eax 放入栈中,给栈中的某个地址赋直的形式,实参的后面(预先留了位置)
疑问:
为什么函数调用完, sp的值没有被修改?栈没有清空,还保留实参?
原来直观的觉得函数调用完,栈应该有 指令去退栈。
恩,恩,
函数运行时,入栈和出栈是代码本身实现的。
要保留一个值就push。
要使用或恢复就pop。
使用的过程就已经出栈了啊。
额,额。
那有没有一些数据是函数本身的数据呢?比如 常量,这个总要储存把。用完就要丢掉把。
恩,先测试函数的局部数据,看看编译后在内存中是怎么回事,会不会清栈。
5)被调函数有局部变量
代码:
int count(int a,int b);
int HariMain(void)
{
volatile int sum =count(1,2);
io_hlt();
}
int count(int a,int b)
{
int c;
int arrayint[2]={5,10};
int i;
for(i=0;i<2;i++)
{
c=c+arrayint[i];
}
c=c+a+b;
return c;
}
编译list:
8 [SECTION .text]
9 00000000 GLOBAL _HariMain
10 00000000 _HariMain:
11 00000000 55 PUSH EBP
12 00000001 89 E5 MOV EBP,ESP
13 00000003 50 PUSH EAX
14 00000004 6A 02 PUSH 2
15 00000006 6A 01 PUSH 1
16 00000008 E8 0000000A CALL _count
17 0000000D 89 45 FC MOV DWORD [-4+EBP],EAX
18 00000010 E8 [00000000] CALL _io_hlt
19 00000015 C9 LEAVE
20 00000016 C3 RET
21 00000017 GLOBAL _count
22 00000017 _count:
23 00000017 55 PUSH EBP
24 00000018 89 E5 MOV EBP,ESP
25 0000001A 52 PUSH EDX
26 0000001B 52 PUSH EDX
27 0000001C 8D 4D FC LEA ECX,DWORD [-4+EBP]
28 0000001F 8D 55 F8 LEA EDX,DWORD [-8+EBP]
29 00000022 C7 45 F8 00000005 MOV DWORD [-8+EBP],5
30 00000029 C7 45 FC 0000000A MOV DWORD [-4+EBP],10
31 00000030 L7:
32 00000030 03 02 ADD EAX,DWORD [EDX]
33 00000032 83 C2 04 ADD EDX,4
34 00000035 39 CA CMP EDX,ECX
35 00000037 7E F7 JLE L7
36 00000039 03 45 08 ADD EAX,DWORD [8+EBP]
37 0000003C 03 45 0C ADD EAX,DWORD [12+EBP]
38 0000003F C9 LEAVE
39 00000040 C3 RET
根据汇编指令大概理解和预测实验点:
1)这次被调函数,有一个局部变量。Int 的数组。
直接查看 被调函数返回前的堆栈情况把。
PUSH EDX
PUSH EDX
(上2条只想达到效果 sub esp 8??)
MOV DWORD [-8+EBP],5
MOV DWORD [-4+EBP],10
2)被调函数执行指令LEAVE后
Leave 等同
MOV SP,BP
POP BP
没有了被调函数的局部变量。
只是修改了esp。也就是只移动了栈顶位置。
3)Ret 后
LEAVE:释放当前子程序在堆栈中的局部变量,使BP和SP恢复成最近一次的ENTER指令被执行前的值。
MOV SP,BP
POP BP
哦。看到leave。完美解释了上一个疑问。
有局部变量的函数。用leave 指令。会修改 sp 。mov sp ,bp。
(后面测试,也可以不用leave,直接ADD ESP,16 ,简单达到修改栈顶地址目的)
也就是修改栈顶位置来达到 调用完函数后,栈丢弃被调者的局部变量。
结论:c 编译器,
1)调用函数时,函数有局部变量,会通过
sub esp xxx.
MOV DWORD [-8+EBP],yyy
达到把函数局部数据压栈的效果
2)当返回时,用leave 或ADD ESP,16 改变栈顶地址来达到清栈的效果。
关于c函数每次都有3条这样的指令
PUSH EBP
MOV EBP,ESP
。。。。。。。
POP EBP
的理解。
可能不太正确。
函数的调用,
首先代码方面
代码的进入函数和退出函数,因为有call 和ret 成对出现。所以没有问题。
那么数据方面呢。
假如A是一个函数,又假如我们使用esp来定位所有非静态数据。A(int x,int y)
用esp 栈顶代表返回地址。
用 esp+4代表第一个参数x
只要碰到变量x。编译器就用esp+4来替换。
但是假如a有局部变量。栈顶随便在变。Esp+4就不再是实参x了。
所以我们假如一进入函数就把esp 赋直给一个寄存器呢,比如和ebp。
那么ebp+4,就永远代表参数x了。我们可以把函数一些固定的数据放入栈中(参数,返回地址,函数的返回直)。用ebp+x 的形式来表示这些直,一些程序运行中可以改变大小的变量,如char * c;那就保留一个它的地址。而实际数据放入堆中。
而用ebp-x 来代表局部数据(所以编译器会把局部数据的代码往前编译出来?不管你定义局部变量的代码写在那里,总是放到临时变量push栈之前。以防之后有push临时变量的 指令?)。
那么函数a就可以准确找到包括参数和局部变量的所有数据。一个寄存器就解决了所有问题?
我想把ebp 叫做“准绳线”。
如果A 调用c函数,c函数和a一样聪明。一进入函数
1,push ebp ,先把a的ebp“绳子”放入栈中。
2,mov ebp,sp 把esp 赋直给ebp寄存器,建立c自己的“绳子”
那么c也可以和a 一样准确找到包括参数和局部变量的所有数据.
1)[ebp] 储存 a 的ebp。
2)[epb+4] 储存 返回地址。
3)[ebp+8] 储存 第一个参数
4)[epb-4] 储存 第一个局部数据。
当c返回时,清栈,从那里开始清呢。慢着。Ebp那么重要。首先要把a 的ebp“绳子” 找回来啊。
这个时候ebp 可是c的“绳子”(mov ebp ,esp )。那么a的ebp呢,不能丢啊,那么重要,记得有(push ebp mov ebp ,esp 两条死都不分开好基友指令). 所以如果我们mov esp ,ebp,那么就达到了清栈的效果,而且现在栈顶的数据就是a的ebp“绳子”了。 那么我们再继续POP eBP,呵呵,寄存器ebp就是a 最重要的ebp了。执行ret吧,现在栈顶是a的返回地址了。我们回到了a 。而且ebp,寄存器保留了对a来说最重要的 准绳地址。
MOV SP,BP POP BP 也是另外2条不分开的好基友指令,所以刚催有合体技能,leave指令。
慢慢记得好像,编译原理是有一个活动记录,这个ebp就是里面提到的top_sp
所以大概流程:
参数入栈,返回地址入栈, ebp入栈放入调用者的绳子,mov ebp,sp设置被调者的绳子
有局部变量,改变sp, sum sp sizeofvar.
用mov [ebp-x],aaa .来压入数据到栈。
最后,有函数局部数据采用 leave (mov sp,ebp 用被调者的绳子来清栈 ; pop ebp找回准绳) 或者ADD ESP,16。。。。
结束 ret 。清掉当时压入的返回地址,最后搞了一圈回来只保留了实参,返回到调用函数代码。
函数一个一个嵌套调用的话,栈的数据越来越多。但当一个函数返回到上一个函数时,栈顶只保留被调函数的参数而已。一层一层返回,栈最终只有主函数的数据和主函数本身直接调用的函数的参数。
但是想到这里,因为每次清栈,都会保留调用函数的参数。那么a调b。回来,a再调c。那不是栈会保留b和c的参数?
Ok,验证一下。
HariMain 调用count和fint1
int count(int a,int b);
int fint1 (int a,int b);
int HariMain(void)
{
volatile int sum =count(1,2);
sum=sum+fint1(5,6);
io_hlt();
}
int count(int a,int b)
{
int c;
int arrayint[3]={5,10,12};
int i;
for(i=0;i<3;i++)
{
c=c+arrayint[i];
}
c=c+a+b;
int int1=fint1(1,2);
c=c+int1;
return c;
}
int fint1 (int a,int b)
{
return a+b;
}
果然,回到a的时候。栈比调用前多了4个int 数据。
没想到,之前老想不太明白的东西,边写边做,边反问。自己清晰了不少。
所以有最后一个疑问,c 编译器为什么栈要保留实参?
不可以先把返回地址入栈,再入参数?
那么指令可以用
mov sp ebp,pop ebp
add sp (由编译器计算所有参数的size)
ret .
这样不是更干净吗?
ebp 寄存器的值是当前函数的栈基址.而栈基址的里面的数据是调用者的栈基址.
就是说在内存的一个地址,也就是当前函数的栈基址写入了调用者的栈基址.
ebp寄存器的数据,一直都是当前函数的栈基址。一般 ($ebp)+8 <寄存器的数据+8> 就是第一个参数。($ebp)-偏移地址就是局部变量。
而 *($ebp)〈寄存器的数据再到内存求数据〉 是 调用者的栈基址,所以被调用者,如果不是语法限制,其实是可以很方便得到调用者的所有数据。
c 函数调用产生的汇编指令和数据在内存情况(1)的更多相关文章
-
c 函数调用产生的汇编指令和数据在内存情况(2)
c 函数调用产生的汇编指令和数据在内存情况(1) 一直对函数调用的具体汇编指令和各种变量在内存的具体分配,一知半解.各种资料都很详细,但是不实践,不亲自查看下内存总不能笃定.那就自己做下. 两个目的: ...
-
C语言函数调用过程,汇编角度查看
C语言函数调用过程,汇编角度查看 把函数的参数按照调用约定压栈或者存储到寄存器中 调用要使用的函数,先把调用者的地址入栈,方便回来 跳转到函数 把函数使用到的一些寄存器压栈,避免修改寄存器的值 执行函 ...
-
iOS图片加载到内存中占用内存情况
我的测试结果: 图片占用内存 图片尺寸 .png文件大小 1MB 512*512 316KB 4MB 10 ...
-
php测试程序运行时间和占用内存情况
php测试程序运行时间和占用内存情况: $HeaderTime = microtime(true);//参数true表示返回浮点数值 /** *CODE */ printf(" total ...
-
Android内存管理(5)*官方教程:Logcat内存日志各字段含义,查看当前内存快照,跟踪记录内存分配,用adb查看内存情况时各行列的含义,捕获内存快照的3种方法,如何让程序暴漏内存泄漏的方法
Investigating Your RAM Usage In this document Interpreting Log Messages 内存分析日志中各消息的含 ...
-
linux 查看cpu个数,内存情况,系统版本
查看cpu个数 总核数 = 物理CPU个数 * 每颗物理CPU的核数 总逻辑CPU数 = 物理CPU个数 * 每颗物理CPU的核数 * 超线程数 查看物理CPU个数 cat /proc/cpuinfo ...
-
Linux 查看进程消耗内存情况总结
在Linux中,有很多命令或工具查看内存使用情况,今天我们来看看如何查看进程消耗.占用的内存情况,Linux的内存管理和相关概念要比Windows复杂一些.在此之前,我们需要了解一下Linux系统下面 ...
-
使用jconsole分析内存情况-JVM
JVM调优分析演练: Jconsole中对内存为如下结构: 原始代码: public static void main(String[] args) { BigInteger [] pArr=new ...
-
Rails内存的问题 Java内存情况
Rails内存的问题 Java内存情况 一个txt文件,100M,300万行,都是坐标数据: 需要进行坐标的变换.计算.比较: 在Rails中使用Ruby进行计算,会导致内存超过1.5G,最后溢出而亡 ...
随机推荐
-
MVC、MVP、MVVM、Angular.js、Knockout.js、Backbone.js、React.js、Ember.js、Avalon.js、Vue.js 概念摘录
注:文章内容都是摘录性文字,自己阅读的一些笔记,方便日后查看. MVC MVC(Model-View-Controller),M 是指业务模型,V 是指用户界面,C 则是控制器,使用 MVC 的目的是 ...
-
h5调用摄像头
<!DOCTYPE html><html lang="zh-CN"><head> <meta charset="UTF-8&qu ...
-
Web应用程序整体测试基础——单元测试
近年来,随着基于B/S结构的大型应用越来越多,Web应用程序测试问题也在逐步完善中.但Web应用程序测试既可以在系统开发中实施,也可以独立于系统单独完成,这取决于Web应用程序的复杂性和多样性.同时程 ...
-
JavaScript HTML DOM 元素(节点)
添加和删除节点(HTML 元素) 创建新的 HTML 元素 如需向 HTML DOM 添加新元素,您必须首先创建该元素(元素节点),然后向一个已存在的元素追加该元素. 例如:这段代码创建新的 < ...
-
(笔记):组合and继承之访问限制(二)
上篇简单介绍了public与private的基本使用.private的访问限制相对复杂.针对这种访问属性,我们会想到有没有一种方式可以无视这种属性.答案是:有.我们可以通过friend的方式(可以破解 ...
-
洛谷P5072 [Ynoi2015]盼君勿忘 [莫队]
传送门 辣鸡卡常题目浪费我一下午-- 思路 显然是一道莫队. 假设区间长度为\(len\),\(x\)的出现次数为\(k\),那么\(x\)的贡献就是\(x(2^{len-k}(2^k-1))\),即 ...
-
实战c++中的vector系列--vector&;lt;unique_ptr&;lt;&;gt;&;gt;初始化(全部权转移)
C++11为我们提供了智能指针,给我们带来了非常多便利的地方. 那么假设把unique_ptr作为vector容器的元素呢? 形式如出一辙:vector<unique_ptr<int> ...
-
Android Hook框架adbi源码浅析(二)
二.libbase 其实上面加载完SO库后,hook的功能我们完全可以自己在动态库中实现.而adbi作者为了方便我们使用,编写了一个通用的hook框架工具即libbase库.libbase依然在解决两 ...
-
MySQL死锁问题分析及解决方法实例详解(转)
出处:http://www.jb51.net/article/51508.htm MySQL死锁问题是很多程序员在项目开发中常遇到的问题,现就MySQL死锁及解决方法详解如下: 1.MySQL常用 ...
-
HashMap相关总结
1.HashMap:根据键值hashCode值存储数据,大多数情况下可以直接定位到它的值,但是遍历顺序不确定.所有哈希值相同的值存储到同一个链表中 Ha ...