个人网站备案 淘宝客,南京做网站建设搭建的公司,专注营销型网站建设,wordpress仿seowhy基础指南模板文章目录 一#xff0c;什么是寄存器 二#xff0c;栈和帧 前言
我们在学习c语言程序的时候#xff0c;是不是有很多的疑问#xff0c;如
1#xff0c;为什么形参不可以改变实参
2#xff0c;为什么我们编写程序的时候会出现烫烫烫......这个乱码
3#xff0c;那些局… 文章目录 一什么是寄存器 二栈和帧 前言
我们在学习c语言程序的时候是不是有很多的疑问如
1为什么形参不可以改变实参
2为什么我们编写程序的时候会出现烫烫烫......这个乱码
3那些局部变量和全局变量为什么是全局变量先在程序中出现局部变量在后面出现
4为什么会出现栈溢出错误栈的大小是怎么弄出来的
5为什么文件的查找数据需要用流但是printf和scanf也是寻找数据为什么不用设置流难道提前设置好了
等等一系列的问题接下来我们就要来学习函数栈帧来知道这些问题 一.什么是寄存器
具有存储功能的硬件
在计算机中具有的存储功能的硬件有哪些呢
硬盘 -- 内存 -- 高速缓存 -- 寄存器
(从左到右)
访问的速度和缓存的速度是在增加的 容量的大小是在减少的 价格的大小是在增加的
如果我们考虑外部的存储的话就是这样的
磁带光盘 -- 硬盘 -- 内存 -- 高速缓存 -- 寄存器规律和上面的是一样的
这里我们生活中所用的u盘其实就是硬盘只不过把他取出来了 寄存器
存储的空间只有4Byte为的存储空间这里说的是32位寄存器因为很广泛访问速度也是最快的
那为什么寄存器的访问速度是最快的呢
因为寄存器是集成在cpu上面的与内存不同它是一个独立的空间 寄存器的分类
(寄存器中的E其实是extend的英文缩写表示把16位寄存器扩展到32位寄存器没有带E的就是16位寄存器带E的就是32位前面这种在x86框架情况是对的在其他框架不适用如在x86-64架构也称为AMD64中寄存器被扩展到64位如RAX, RBX, RCX, RDX等这些寄存器可以访问其32位和16位部分我们要考虑框架和文件)
一般的寄存器EAX EBX ECX EDX
ax累积暂存器 bx基底暂存器 cx计数暂存器 dx资料暂存器
索引寄存器ESI EDI
si来源索引暂存器 di目的暂存器
堆叠基底寄存器ESP EBP
sp堆叠指标暂存器 dp基底指标暂存器 寄存器的用途
每个寄存器都是有自己各自的专长与特别之处
一般寄存器
1.EAXAaccumulation 积累 / accumulate 计算
1为“累加器”进行加法减法乘法除法运算时被当做累加器使用体现出加法的原理
为整数与浮点数计算的核心寄存器之一
2用于保存计算的结果和数据
2.EBX Bbase 基本
1用于保存基地址的信息常用于访问内存的数据与元素
2用于保存指针和地址信息方便与其他的内存地址进行运算
3.ECXCcounter 计数
1通常被用为计数器放到循环与迭代操作在循环里面ECX是可以用于保存循环次数的然后自己递减直到0就停止
4.EDXDdata 数据
1保存数据和计算结果的临时存储
2用于存储整数除法的余数
总结一下一般寄存器
A是计算所以是作为四则运算的寄存器累加器还有一个额外的功能存储数据和结果
B是基本所以是保存基地址用于访问里面的数据与元素因为存的是地址所以就很方便利用地址运算
C是计数所以一般存储循环的条件值然后自己会自己递减到0然后进行结束
D是数据这里就是存储数据的所以可以存储数据和计算结果还有整数的乘法和除法的余数
索引暂存器
1.ESISsource 源头
1这里主要存储指向源数据的指针和索引它经常与字符串一起使用指示要操作的字符串的首地址
这里的“源数据”指的是在执行某些指令时需要从中读取数据的内存位置或数据结构
2.EDIDdestination目的地
1这里存储目标数据的指针和索引通常指向目标字符串的起始位置以指示存储的位置
总结一下索引寄存器
ESIs 源头这里是存储指向源数据的开头以便于可以方便操作这个数据
EDId 目标这里是存储目标数据的开头方便提示这个存储这个目标数据的位置
堆叠基底暂存器
1.EBPB 基底
1存储堆栈帧的基地址的指针很重要
2在函数调用的和返回的过程中主要用于维护栈帧的上下文的数据信息以便正常访问局部变量传递参数和保存返回地址
2.ESPS stack 栈顶
1存储堆栈帧的栈顶的指针很重要
2在函数调用的和返回的过程中主要用于管理栈帧的内存当压入栈的数据越多ESP也会相对移动可以理解为减少
3.EIPI instruction
1存储下一条需要进行的指令cpu根据EIP来跟踪程序执行的流程执行完自动更新EIP指向下一个指令
总结一下堆叠基底暂存器
EBPB 基底主要储存基底的地址所以可以很好管理这些数据
ESPS 栈顶主要储存栈顶的地址所以可以根据栈顶指针的移动来管理栈的内存
EIP I(交互)主要是存储下一个指令方便后续的程序的进行可以理解为是为了进行交互 二栈和帧
1栈是什么呢
数据依次存入栈中去元素的时候最先放入的元素最后拿出来最后放入的元素最先拿出来这个就是栈
可以理解为现实生活中的放东西与取东西
2函数栈帧的概念
在计算机科学中有这么一个概念它是指在调用函数的时候系统为函数调用创建一块内存区域这块内存区域存储了函数的局部变量函数的参数返回地址等信息
这个时候ESP和EBP是会去维护这个函数的空间在函数运行的时候ESP栈顶指针指向栈的头部EBP指向栈的底部假设我们整一个main函数 这个就是我们运行到main函数的时候所形成的栈帧
3main函数的压栈过程
在运行调试程序的过程中我们可以调用堆栈时发现main函数其实也是被别的函数调用的 分别是_tmainCRTStartup和mainStarup函数调用的逻辑顺序为
mainStatrup -- _tmainCRTStartup -- main
这个是mainStartup压栈 这个是_tmainCRTStartup压栈 mainStarup函数非Unicode版本的例程它负责main函数的初始化
_tmainCRTStartup函数Unicode版本的例程他也是负责main函数的初始化但是它是支持Unicode字符的初始化
Unicode的大概理解把我们的文字组合起来让计算机认识并表示出来
初始化1环境的设置全局变量的初始化等... 2命令行参数的解析 3I/O流的初始化这个时候我们才可以用到这个printf和scanf的函数输入输出4信号处理 5其他系统的初始化 这个可以理解很多问题了函数的压栈就是这样
三样例程序的压栈
#includestdio.h
int add(int x,int y)
{int z 0;z x y;return z;
}
int main()
{int a 10;int b 10;int c 0;cadd(a, b);printf(%d, c);return 0;
}
1main函数的构造
以汇编语言讲述
常见的汇编语言这个是截取一个学长的图片 这个是我们main函数前面还没有调用add函数时候的汇编语言 第一步
push的作用是把这个东西压入栈中
这里的意思是把ebp这三个压入栈中
图示有两种情况会是那种呢这里的那个ebp下面的空间是_tmainCRTStartup的 由于2019的vs不可以监视到edp的改变我们可以通过这个来看来判断是左图还是右图
我们来监视esp的值是多少
然后打开内存块区寻找ebp的地址记住这里的ebp不是ebp栈底指针而是压入到栈的寄存器一定不要搞混了我们去内存块区寻找一下这个ebp的地址 这里看到地址为0x0137FA34 我们把这个20445748十进制转换一下换成十六进制看看是否相同 由于大小端的问题所以这个是倒着存的如果想知道为什么倒着存储可以去了解一下大小端这里我们可以观察到这个esp栈顶指针指向的地址就是ebp上面右图是对的所以我们每当我们压入栈数据的时候这个esp是实时进行变换的 第二步
mov的作用是把后者赋值给前者
所以这里的作用是把esp的地址赋给ebp那这里的图是什么样的呢 第三步
sub的英文的意思是减去的意思前者减去后者这么多
注我们下面是高地址上面为低地址
在这里是向上增长0E4h228这么多的空间这里的h是十六进制的后缀 由于vs2019无法观察这个过程所以建议读者可以去寻找vs2013去学习这样可以更加直观所以这里就是在创建一个空间 第四步
这里的ebxesiedi都是非易失寄存器什么事非易失寄存器呢就是在电源关闭的时候也可可以保存其中的内容那么我们怎么画这个时候的图呢要记得我们在操作的时候这个esp栈顶指针是会改变的 这个操作就是把这几个寄存器压入栈里面去 第五步
lea全名是lead effective address加载有效地址
从此处正式加载当前函数的有效栈区域
这里是把ebp-24h这个地址存放在edi里面(我们来回顾一下edi在下面这个是存储一个这个这么大的空间的指针为了准备一个内存区域用于存储函数的局部变量等的数据有了这个edi才可以找到这个地方的首地址才可以确保数据的正常存储
注因为这个vs2019这个是根据你写的代码所写的汇编语言每个编译器都是不一样的如果你在main函数里面不断写入变量这个就是会改变的其实VS2013真正的写的是[ebp-0E4h]的
栈开辟之后是不可以改变的如果超出了栈就会报栈溢出错误一般来说都是200多M的大小一个函数这里的操作就是正式的开辟一个空间
EDIDdestination目的地
1这里存储目标数据的指针和索引通常指向目标字符串的起始位置以指示存储的位置
我们来看看vs2019变化里面的变量后会怎么样
这个是我们加了变量之后这个变成了ebp-48h了所以可看到不同的编译器所编写的栈帧都是不一样的 第六步
这里就是把9赋值给ecx寄存器0CCCCCCCCh赋值给eax中 第七步
我们把这个代码拆开来理解
1rep这个是一个前缀指令用于重复执行紧跟其后面的指令直到ecx为0
2stos全名store String存储字符串
把eax的内容赋值到edi所指向的地址中并把edi递增逐步的把这个全部填满这个空间
3dword全名double word指4个字节word是指2个字节
4ptr 这个是操作数的大小提示符号比如这里就是告诉编译器这个是4个字节4个字节输入顺便告诉编译器接下来是按照特定的字符大小进行操作的比如这里的4个字节
5es这个是寄存器的前缀用于指定内存操作
6[edi] 这个是指是edi这个指向的内存
总体来说就是每次想edi指向的地址一4个字节的大小不断地把eax存储的值传入进去
那么这里的ccccccccc是什么呢我们之前不是会遇到烫烫烫这一长串的代码嘛其实就是这个ccccccc弄出来的比如变量为初始化打出的乱码就是烫烫烫这个烫烫烫实质就是cccccc 程序走到了这里main函数的帧栈正式开辟成功这个是有esp和edp形成的区域为一个函数的作用域接下来就是执行main函数内部的东西了 2生成局部量 这个我们以a为例子
0Ah其实就是把十六机制转换成十进制这个就是10的意思然后这个后面就是把0Ah赋给a这个地址其他都是一样的 3main函数的总结
接下来呢我们main函数里面的就基本结束了后面就是add函数的了我们来总结一下这个main函数是怎么操作的
1首先就是把一个ebp压入栈中然后ebp这个是用来代表基地址的或者就是说存放了基地址
2我们利用edp这个指向esp的位置是我们这个ebp进行调位置指向main的基地址
3我们利用esp这个减去一个数值使esp来想上移动然后就是给main函数一个预留一个空间
4我们利用edi的赋值正式把这个地址赋值给edi可以把更好的寻找到这个空间的首地址所以就是相当于正式开辟了一个内存空间这个空间大小就是我们所预留的空间大小
5然后对于这个空间里面进行初始化把这里面填满c这个东西然后给ecx传值
6对于局部变量的生成利用mov这个指令 接下里就是add这个函数的分析了
4调用函数与传参 前面四行代码
前面两行显然是把b的地址的值通过mov的指令”拷贝”到eax里面去这个mov是其实是转移的意思也可以理解为拷贝的意思然后把eax压入到栈中
后面两行显然也是把a的地址的值通过mov的指令“拷贝”到ecx里面去然后就把ecx压入到栈里面去
我们来画一下这个图示 为什么我们要有这四行指令这个其实就是把形参压入栈里面去了这里我们就可以了解到其实形参和实参是处于两个独立的空间的所以我们就可以知道为什么形参的改变不了实参了答案就在这里所以这四行代码就是建立形参用的 一到四行是为形参做准备那这样的压入栈中真的可以把参数传入到函数里面吗调用的函数该怎么使用我们的参数 我们继续往下看
第五行
call指令这里其实就是一个转移指令符转移到另外一个地方去同时也是为了完成转移后完成原区域的下一个指令那他是怎么实现这个功能的呢我们继续详细了解call指令时把我们下一个指令压入栈中然后这样的话就可以实现转移后可以返回到原地
简单来说原地插个眼后传送去支援最后还可以返回到线上做到有来有回
我们来看看真的是这样嘛由于vs2019是不支持看这个的所以你们可以下载vs2013点击F11然后就可以看到类似于这个的声明 这个就是对于add的一个声明然后这里有个jmp的指令这里就是跳入add函数的意思为转换的操作我前面画的那个方框是那个地址应该是call后面的那个地址
应该是相同的这个我是找了别的图所以不同因为vs2019弄不出来我在网上找了一个这个指令就是想告诉读者有这个操作 接下来就是正式跳转到add函数了 5Add函数
add函数的创建 我们先来看这个这个是不是似曾相识没错跟我们创建main函数的方式一模一样读者可以尝试自己去解读一下这样可以让自己的形象更加深刻我把答案写到下面了 我们来看看现在的栈帧的图该怎么画 变量的生成 运算 这个局部变量的形成是跟前面一样的这里就不多讲了我来看看后面的 第一句是把形参x放入到eax中因为eax的用处有可以用与加法然后第二句再把这个y放入到这个eax中然后根据add指令执行相加我们就的道了结果然后再把eax的值拷贝给z这个变量这个z里面去这个就是运算形参的压入顺序是按照从左往右的根据你设置的函数
函数的返回值和函数返回与销毁的实现
我们按照前面所学的xyz都是在函数调用完会被销毁的
问题1我们该怎么获取这个返回值
问题2esp栈顶指针和ebp栈底指针该何去何从 我们先来看这个return这个代码
这里是把z的值存储到eax中因为我们知道eax的一个用处是存储数据和运算结果把他临时放入到eax中就可以把值返回了第一个问题就迎刃而解了
当然值超出了eax的范围就要用到其他寄存器存储了比如esi等其余寄存器存储 我们再来看看后面的销毁与返回怎么实现的呢我们来看这个指令 pop指令指跳出栈将元素弹出栈以此释放掉
这个是把是三个非易使寄存器给弹出去释放掉他们三个注意这个esp栈顶指针的位置是会变化的
这里是弹出三个寄存器
这个的用处栈不是弹出来了那三个寄存器嘛然后就要收缩调整栈你看0CCh不正是我们之前所弄出来的空间大小嘛
这里是栈的收缩调整 这两行指令其实就是检查是否有栈溢出的哪些错误
cmp这个指令是比较两个
比较基指针EBP和栈指针ESP的值。这通常用于检查栈是否正确对齐或者在调试时检查栈是否被破坏。
call这一段 调用运行时检查函数 __RTC_CheckEsp 这个函数可能是用于检查栈溢出或栈保护的。 051244h 是该函数在内存中的地址。这个调用可能是由编译器插入的用于在运行时检查栈指针是否在函数调用后仍然有效以防止栈溢出攻击或检测栈损坏
这两个是检查安全性的 我们来看后面的指令
第一行就是把ebp的值赋给esp然后ebp会读取地址然后转移到之前main函数的基地址之后再让ebp读取之前的那个地址这里的pop是pop另外一个功能是读取数据的功能实现了这个esp和ebp的转移
根据这个转移最后把这个弹出即可然后就可以跟着下一个指令了
这里的ret是把栈顶的字节安远出栈然后交给EIP来处理这样就可以紧接着这个后面程序的执行即可
我来总结一下add函数的过程
1我们先进入函数的调用先把形参压入到栈里面然后利用call进入到那个函数的声明的地址哪里并且把下一个指令压入到栈里面去然后再利用jmp跳入到那个函数里面
2我们在把局部变量弄出来然后利用add和eax这两个弄西进行运算最后赋值给z
3然后把z的值暂存储在eax中
4运行返回时我们就把三个寄存器弹出去然后ebp会赋给espebp会读取之前的ebp地址进行跳转然后就可以实现这个esp和edp返回原位置
5利用ret来实现后面的程序即可 总结
上面的文章里面都有每小段的总结我们可以根据这些可以解决很多问题