了解汇编语言可以更加深刻的理解高级语言的本质,完全理解以前只是知道却又不清楚为何的知识,好比:程序员
使用 Visual C++ 6.0 建立一个 Win32 Console Application项目,建立一个数组:编程
#include "stdafx.h"
int main(int argc, char* argv[])
{
int array[] = {3, 4};
return 0;
}
复制代码
输入光标在 return 0 位置按 F9 插入断点运行项目,而后鼠标右击出下拉菜单点击 Go To Disassembly 查看反汇编代码:数组
基本能够看出来汇编代码的意义:将 3 存在内存地址为 ebp-8 的位置上,将 4 存在内存地址为 ebp-4 的位置上。sass
将高级语言代码修改成:安全
int main(int argc, char* argv[])
{
// int array[] = {3, 4};
struct Person {
int no;
int id;
} p = {3, 4};
return 0;
}
复制代码
反汇编查看对应的汇编代码为:bash
能够发现,对应的汇编代码一摸同样,因此经过汇编没法获得对应的高级语言代码,由于对于汇编来说,上面的两段代码都是开辟8个字节的内存空间,分别存3和4。经过汇编语言看逆向推导,是没法知道高级语言是建立了数组仍是结构体的。这就是为何汇编语言没法还原成高级语言。数据结构
经过查看对应的汇编代码能够看到:架构
直接将4传给对应的内存空间,并无调用任何函数,因此sizeof并非一个函数,而是一个编译器特性,在编译后直接转成了对应的汇编代码。函数
同理在Xcode中也是同样的:工具
总线:即一根根导线的集合。
每个CPU新片都有许多管脚,这些管脚和总线相连,CPU经过总线和外部器件进行交互。
总线的分类:
地址总线决定了CPU的寻址能力,8086地址总线宽度是20,因此它的寻址能力是1M(2^20)。
寻址能力的计算:首先明白总线就是导线,导线可以传递的是电信号,电信号分为两种:高电平信号、低电平信号,高电平信号即 1,低电平信号即 0。假如总线总线的宽度是3,那么3根导线高电平为1,低电平为0,它们最大可以传递的值只有2^3 种:000,001,010,011,100,101,110,111。
8086地址总线宽度是20,能够表示2^20种不一样的地址值,它的寻址能力就是2^20即1M。
数据总线决定了CPU单次数据的传送量,也就是数据传送的速度。8086的数据总线宽度是16,因此单次最大可以传递2个字节的数据。
单次数据传送量的计算:数据总线的宽度是16,同地址线同样,16根线表明16位0或1,即16位二进制数据,一次最多可以传送16个二进制位。一个字节是8位,16位即2个字节。因此8086单次可以传递的最大数据量就是2个字节。
8088的数据总线宽度是8,8086的数据总线宽度是16,分别向内存中写入89D8H时(89D8H即16进制的89D8,汇编语言中末尾加H代码16进制)。一个16进制表明4个二进制位,两个16进制表明8个二进制位即1个字节,四个16进制即2个字节。
由于8088数据线宽度是8,一次只能传递一个字节,因此8088传递89D8H须要传2次,第一次传D8,第二次传89。而8086只须要一次就可以将89D8传递完成。
控制总线决定了CPU的控制能力,表明CPU有多少种控制能力。
1个CPU的寻址能力位8KB,那么它的地址总线宽度为 ( 13 )
2^10 = 1KB,2^10 * 8 = 8KB = 2^10 * 2^3 = 2^13。
复制代码
8080、808八、8028六、80386 的地址总线宽度分别为 16 根、20 根、24根、32根,则它们的寻址能力分别为:( 64KB )、( 1MB )、( 16MB )、( 4GB )。
2^10 = 1KB,2^20 = 1MB,2^30 = 1GB
16根:2^16 = 2^10 * 2^6 = 64KB
20根:2^20 = 1MB
24根:2^24 = 2^20 * 2^4 = 16MB
32根:2^32 = 2*30 * 2^2 = 4GB
如今知道为何32位的Windows最大只能支持4G内存的把
复制代码
8080、808八、808六、8028六、80386 的数据总线宽度分别为 8根、8根、16根、16根、32根,则它们一次能够传送的数据为:( 1B )、( 1B )、( 2B )、( 2B )、( 4B )。
8个二进制位(0000 0000):1B
16个二进制位(0000 0000 0000 0000):2B
32个二进制位(0000 0000 0000 0000 0000 0000 0000 0000):4B
复制代码
从内存中读取 1024 字节的数据,8086至少须要读( 512 )次,80386至少须要读( 256 )次。
读取数据看数据总线的宽度:
8086:16,一次能够读2个字节,1024 / 2 = 512
80386:32,一次能够读4个字节,1024 / 4 = 256
复制代码
全部的内存单元都有惟一的地址,这个地址叫作物理地址。
8086CPU的地址总线是20根,那么它可以访问的内存空间的地址值范围即 0x00000 - 0xFFFFF(上面已经说明过,一个16进制位=4个二进制位),经过这个范围能够定位2^20个不一样的内存单元,因此8006的内存空间大小为1M。
下面是8086内存空间的示意图:
上面提到8086的地址总线宽度为20,寻址能力为1M,可是实际上8086是一个16位架构的CPU,它内部可以一次性处理、传输、暂存的数据只有16位。这就意味这8086实际上只可以直接送出16的地址,可是它的地址总线宽度又是20位,意味这这样就有4位是没法使用的,它的实际寻址能力只可以是64KB。那么它是如何作到实现1M的寻址能力呢,具体步骤以下:
段地址和偏移地址合成物理地址的计算规则:物理地址 = 段地址 * 10H + 偏移地址。
假如8086CPU须要访问地址为 0x136CC 的内存单元。 须要的拆分为:段地址0x1360,偏移地址0x00CC。 物理地址 = 0x1360 * 0x10 + 0x00CC = 0x136CC
经过上面的计算能够得出在16进制位表示下,合成段地址和偏移地址的规律:段地址 * 0x10 + 偏移地址
当段地址必定的时候,根据变化编译地址最多可访问的内存单元数量为偏移地址的范围0x0000 - 0xFFFF,即64KB。
注:段地址和偏移地址计算物理地址并非全部CPU通用的寻址方式,只是8086是比较特殊,它是一个16位架构的CPU,可是地址线宽度为20。其它高级的CPU并无这种状况,即它们没有段地址,也不须要地址加法器,只须要一个偏移地址就可以访问所有内存。
寄存器是CPU很是重要的部件,能够经过改变寄存器的值来实现对程序的控制。不一样CPU的寄存器个数和结构通常都不相同,下面是8086CPU寄存器的结构,8086CPU有14个寄存器,全部寄存器都是16位的。
CPU在对内存中的数据进行运算时,首先将内存中的数据存储到寄存器中,而后再对寄存器的数据进行运算。
汇编语言没有数据类型的概念,它是直接操做内存的,汇编语言的数据存储单位有两个:
好比数据4E20H,高字节是4EH(78),低字节是20H(32)。
0x4E20
0100 1110 0010 0000
|_______| |_______|
高位字节 低位字节
复制代码
数据寄存器由AX、BX、CX、DX组成,虽然上图里边每一个每个寄存器都分红了两块,但它依然是一个寄存器。因为8086以前的CPU是8位的架构,因此8086为了兼容8位的程序,每一个16位数据寄存器均可以看成两个单独的8位寄存器来使用。
AX寄存器能够分红两个独立的8位寄存器,高8位为AH,低8位为AL,BX、CX、DX同理。除了四个数据寄存器以外,其它的寄存器均不能够分为两个独立的8位寄存器。独立的意思是:当AH和AL作为8位寄存器使用时,能够看做它们是互不相关的,形式上能够看做两个彻底独立的寄存器。既然数据寄存器能够看成两个独立的寄存器,那么它们的便可以用整个寄存器的16位存放一个数据,也能够高8位和低8位分别存放一个数据共存放两个数组。
前面关于8086的寻址方式里边提到,8086须要16位的段地址和偏移地址合成20位地址,其中的段地址就由段寄存器提供。段寄存器一共有四个,每一个段寄存器的做用都不相同。
CS和IP配合使用,它们指示了CPU当前要读取指令的地址。任什么时候候,8086CPU都会将CS:IP指向的指令作为下一条须要取出执行的指令。
指令执行的过程:
在内存或者磁盘上中,指令和数据没有任何区别,都是二进制信息。 CPU在工做时,有时候把信息看成指令,有时候看做数据,一样的信息赋予不一样的意义。
CPU根据什么将内存中的数据信息看成指令? 经过CS:IP指向的内存单元内容看做指令。
DS是用来操做内存时提供段地址的,假如须要将内存中10000H 存入1122H,直接这样写是不能够的:
mov 1000H:[0H],1122H
复制代码
由于汇编语言又以下要求:
; 不能直接给DS赋值,须要经过寄存器中转
mov ax, 1000H
mov ds, ax
; 不能直接给内存地址赋值,必须经过DS:[偏移地址]指向内存
; 内存中的10000H位置存入了1122H
mov [0H], 1122H
复制代码
SS配合SP使用,SS:SP指向栈顶元素。后面栈章节中会有更详细的介绍。
mov指令能够修改大部分寄存器的值,好比AX、BX、CX、DX、SS、SP、DS,可是不能修改CS、IP的值,8086没有提供这样的功能。
; 汇编语言中的注释用;
; 将1122H存入寄存器ax
mov ax,1122H
复制代码
mov使用时最好和byte和word配合使用,明确操做的字节数量:
; 假设内存10000H原始值: 1122H
; 8086是小端模式,高字节放在高地址,低字节放在低地址
; 1000:0000 22
; 1000:0001 11
; 准备修改10000H位置的值
mov ax, 1000H
mov ds, ax
; 1000:0000 66
; 1000:0001 11
; 修改后10000H: 1166H
mov [0], 66h
; 1000:0000 66
; 1000:0001 11
; 修改后10000H: 1166H
mov byte ptr [0], 66h
; 1000:0000 66
; 1000:0001 00
; 修改后10000H: 0066H
mov word ptr [0], 66h
复制代码
在高级语言中,不少状况下都须要改变代码的执行流程,好比if...else判断,switch判断等,这些改变代码的执行流程本质上就是改变了CS、IP的指向。可是上面提到不可以直接CS、IP,8086提供了jmp指令:“ jmp 段地址:编译地址 ” 或 “ jmp 某个合法寄存器 ”来完成。
; 修改CS:IP
jmp 23E4:3
; 执行后:CS=23E4H,IP=0003H
; CPU从23E43处读取指令并送入指令缓冲区。
; 只修改IP
jmp ax
; 执行前:ax=1000H, CS=2000H, IP=0003H
; 执行后:ax=1000H, CS=2000H, IP=1000H
复制代码
add是汇编语言中加法操做,add ax, 1111H 指令为将寄存器ax中的值加上1111H再赋值给ax。
; ax=1122H
mov ax,1122H
; ax=2233H
add ax,1111H
复制代码
sub是汇编语言中减法操做,sub ax,0011H 指令为将寄存器ax中的值减去0011H再赋值给ax。
; ax=1122H
mov ax,1122H
; ax=1111H
sub ax,0011H
复制代码
入栈,详见后面栈章节。
出栈,详见后面栈章节。
将0x1234存放在CPU内存中的0x4000位置,大小端的区别为:
小端 大端
0x4000 0x34 0x12
0x4001 0x12 0x34
复制代码
小端模式:808六、x86
大端模式:PowerPC、IBM、Sun
ARM既能够工做在大端模式,也能够工做在小端模式
须要运行和调试8086汇编会好的工具就是这个软件 emu8086,这个软件能够很是方便和直观编写、调试、运行8086汇编,支持Windows平台,软件界面以下:
安装完成后我先尝试使用一下:
打开emu8086,打开后默认就有一个编辑界面,咱们尝试在内存中10003H中写入1234H,编写以下指令后点击emulate按钮执行:
执行后会弹出一个调试窗口,点击窗口顶部的菜单栏view-memory打开内存查看视图:
在内存查看视图修改默认的段地址和偏移地址,查看1000:0000的位置,能够看到内存中1000:0003位置的值都是00H
如今观察调试窗口的信息
左侧是当前全部寄存器的值;中间蓝色的是当前执行指令的位置,蓝色的行数就是当前执行指令的长度;右侧就是当前即将执行的指令。咱们能够发现以下规律:
点击single step执行mov ax, 1000H
点击single step执行mov ds, ax
点击single step执行mov bx, 1234H
点击single step执行:mov [3H], bx
栈是一种具备特殊访问方式的存储空间(后进先出),在栈和队列中有关于栈的数据结构和原理介绍。
; 将ax寄存器的数据入栈
push ax
; 将栈顶的数据送入ax寄存器
pop ax
; 注:8086 push和pop就是以word为单位,没有byte的操做,不须要指定单位
复制代码
如今假设SS=1000H,SP=0004H,AX寄存器中存放着2266H,而且如今栈的内存空间都是存放00H。
下面就是栈的当前内存结构:
push ax 指令执行的步骤:
虽然栈顶相对内存是上移的,可是存入两个字节时,仍是要从栈顶往高拿两个字节的内存存放元素。
如上存入2266H,栈顶上移两位后为:10002H,那么须要用10002H和10003H存放2266H。8086是小端模式,高字节22H放在高地址10003H,低字节66H放在低地址10002H。
接着上面的栈的状态,咱们如今执行指令 pop bx。
注:先从栈顶指针指向的内存位置取两个字节的数据,依然是往高取两个字节:10002H和10003H。按照高字节高地址、第字节低地址的规则,10002H和10003H的存储的值是2266H,将2266H放入bx。
注:观察上图第二步后栈的状态,10002H的值依然是66H,10003H的值依然是22H,即pop操做后,内存中的值是不会清0的,它们还保持着原来的值。假以下次再进行将3399H入栈是,那么33H就会覆盖22H,99H就是覆盖66H。
注意观察上图push操做的SS:SP位置,当栈是空的时候,SS:SP指向的是10004H的位置,push2266H后,最高存放22H的内存是10003H。
假如继续将3399H入栈,那么栈顶指针相对于栈空间的位置关系以下:
汇编语言中栈是不会自动判断栈是否越界的,那么就可能出现以下图push和pop越界问题:
不管是push仍是pop越界都是很是危险的,由于栈外部的内存中可能存放其它任意数据,多是代码、重要数据等,将它们覆盖或者拿出来使用均可能发生不可预知的严重错误。
将10000H-1000FH看成栈空间,初始状态栈是空的,假设AX=001A,BX=001BH,利用栈将AX、BX值交换。
mov ax, 1000H
mov ss, ax
mov sp, 0010H
mov ax, 001AH
mov bx, 001BH
push ax
push bx
pop ax
pop bx
复制代码
下面的代码包含汇编语言的基本指令:
; 将代码段寄存器和咱们的代码段关联起来
assume cs:code
; 代码段开始
code segment
mov ax, 1122h
mov bx, 3344h
add ax, bx
; 正常推出程序 至关于 return 0
mov ah, 4ch
int 21h
; 代码段结束
code ends
; 程序的结束
end
复制代码
汇编语言指令分为2类:
上面的segment和ends的做用是定义一个段,segment表明段的开始,ends表明段的结束:
段名 segment
:
段名 ends
复制代码
一个有意义的汇编程序中,至少要有一个段作为代码段存放代码。
assume 的做用是将代码段和mycode段和CPU中的CS寄存器关联起来。
end 代码程序的结束,编译器遇到end就会结束编译。
下面的代码表明退出程序,int不是整形的意思,是interrupt的简写,表明中断:
; 只要ah是4ch就能够结束
; al是返回码,相似于return 0的0,mov ax, 4c00h
mov ah, 4ch
int 21h
复制代码
中断是因为软件或硬件的信号,使CPU暂停执行当前的任务,转而去执行另外一段子程序。
能够经过 “ int 中断码 ” 实现中断,内存中有一张中断向量表,用来存放中断码处理中断程序的入口地址。CPU在接受到中断信号后,暂停当前正在执行的程序,跳转到中断码对应的向量表地址处去执行中断。
常见中断:
下面是int21对应AH寄存器部分功能对照表
AH | 功能 | 调用参数 | 返回参数 |
---|---|---|---|
09 | 显示字符串 | DS:DX=串地址 | (DS:DX+1)=实际输入的字符数 |
4c | 带返回码结束 | AL=返回码 | 无返回参数 |
汇编语言中可使用db定义数据:
; 定义100个
// 定义一个字节的00H
db 0h
// 定义一个字的数据0000H
dw 0h
复制代码
在数据段定义数据至关于建立全局变量
在栈段定义数据至关于指定栈的容量
在代码段定义数据通常不会这样使用
可使用dup批量的去声明数据:
dw 3 dup(1234H)
复制代码
声明3个1234H:
代码段用存放咱们须要执行的代码。
数据段的指令用于建立数据,数据段的数据在程序开始运行的时候就已经建立好了,至关于全局变量。
栈段就是用来函数执行须要使用的临时空间,通常用于存放临时变量、函数返回后下一条指令的偏移地址。
建立一个包含完整的数据段、代码段、栈段的汇编程序:
assume cs:code, ds:data, ss:stack
stack segment
; 自定义栈段容量
db 100 dup(0)
stack ends
data segment
db 100 dup(0)
data ends
code segment
start:
mov ax, stack
mov ss, ax
mov ax, data
mov ds, ax
mov ax, 1122h
push ax
pop bx
mov ax, 4c00h
int 21h
code ends
end start
复制代码
上面咱们定义的栈段的容量是100,能够看到程序运行后,SP=64H=100。
作为iOS程序员若是了解过iOS内存管理的话必定知道下面的iOS内存布局:
低地址
| 保留段
|
| 代码段(__TEXT)
|
| 数据段(__DATA)
| 字符串常量
| 已初始化数据:已初始化的全景变量、静态变量等
| 未初始化数据:未初始化的全局变量、静态变量等
|
| 堆(heap)⬇️ 地址愈来愈高
|
| 栈(stack)⬆️地址愈来愈低
|
| 内核区
高地址
复制代码
首先看一下是否是和咱们刚刚实现的汇编很类似,有代码段和和数据段。
其中栈地址愈来愈低是否是和咱们刚刚汇编分析的栈push同样,push的时候SP=SP-2,栈顶指针上移,栈顶指针变小,地址变低。
全局变量放在数据段中,即程序运行时就将数据放在数据段中跟刚刚汇编代码中在数据段建立数据是同样的,还有这也解释了为何全局变量的地址在编译就已经肯定好不会再次改变,咱们汇编中在数据段建立的数据地址也是肯定而且不会变化的。
有了上面的基础指令和分段以后,终于能够实现经典程序HelloWorld的输出了:
; 将代码段寄存器和咱们的代码段关联起来
; 将数据段寄存器和咱们的数据段关联起来
; 注:这里的关联并无任何实际操做,至关于给咱们本身的注释而已
; 至关于即便不写这一行也没有关系
assume cs:code, ds:data
; 数据段开始
data segment
; 建立字符串
; 汇编打印字符串要在尾部用 $ 标记字符串的结束位置
; 将字符串用hello作一个标记,方便后面使用它
hello db 'Hello World, Whip!$'
; 数据段结束
data ends
; 代码段开始
code segment
; 指令执行的起始,相似于C语言的main函数入口
start:
; 汇编语言不会自动把数据段寄存器指向咱们程序的数据段
; 将数据段寄存器指向咱们本身程序的数据段
mov ax, data
mov ds, ax
; 打印字符串的参数
; DS:DX=串地址,将字符串的偏移地址传入dx寄存器
; 字符串是在数据段起始建立的,它的偏移地址是0h
; offset hello 即找到标记为hello的数据段字符串的编译地址
; 还能够写成 mov dx, 0h
mov dx, offset hello
; 打印字符串,ah=9h表明打印
mov ah, 9h
int 21h
; 正常退出程序,至关于高级语言的 return 0
mov ah, 4ch
int 21h
; 代码段结束
code ends
; 程序的结束
end start
复制代码
运行程序会显示打印的窗口:
使用call和ret配合能够调用和返回一段其它位置的指令,至关于面向对象语言的中的函数调用:
assume cs:code, ds:data
data segment
hello db 'Hello World, Whip!$'
data ends
code segment
start:
mov ax, data
mov ds, ax
call print
mov ah, 4ch
int 21h
print:
mov dx, offset hello
mov ah, 9h
int 21h
ret
code ends
end start
复制代码
上面的汇编指令看起来很简单,call调用,调用的指令完成ret返回,而后在执行call后面的指令。可是,它内部是如何实现的呢?ret以后,是如何知道继续调用哪一条指令呢?下面就从调试工具来看看究竟是如何实现的,首先指令的调用是根据CS:IP的指向来决定的,咱们要关注CS、IP寄存器的变化,以及各个指令的内存地址,另外这里既然涉及到相似复原的操做,首先想到的就是查看栈里边是否有变化。
首先先知道到即将调用call print的位置,能够发现
下面执行call 0000Ch
下面一直执行到ret
经过上面的分析能够知道call和ret的做用
将下一条指令的偏移地址入栈
执行函数
将栈顶的值出栈,赋值给IP
上面咱们经过call print实现了打印hello world,这里咱们换成另外一种方式,让call print返回须要打印的字符串的偏移地址,ret后打印出来。首先就是考虑如何将字符串的编译地址返回出来。
下面就用数据段实现:
assume cs:code, ds:data
data segment
db 100 dup(0)
hello db 'Hello World, Whip!$'
data ends
code segment
start:
mov ax, data
mov ds, ax
call print
mov dx, [0]
mov ah, 9h
int 21h
mov ah, 4ch
int 21h
print:
mov [0], offset hello
ret
code ends
end start
复制代码
assume cs:code, ds:data
data segment
db 100 dup(0)
hello db 'Hello World, Whip!$'
data ends
code segment
start:
mov ax, data
mov ds, ax
call print
mov dx, ax
mov ah, 9h
int 21h
mov ah, 4ch
int 21h
print:
mov ax, offset hello
ret
code ends
end start
复制代码
先执行函数sum后,将eax寄存器中的值存入int c中,打印c的值,若是c=1+2=3,那么就证实函数返回结构存放在eax中。
注:eax至关于8086的ax。
#include "stdafx.h"
int sum(int a, int b) {
return a + b;
}
int main(int argc, char* argv[])
{
sum(1, 2);
int c = 0;
__asm {
mov c, eax
}
printf("%d", c);
getchar();
return 0;
}
复制代码
输出结果:
高级语言的函数几乎都是由 返回值-参数-函数名 构成的,好比:
int add(int a, int b) {
return a + b;
}
复制代码
咱们以前已经在汇编函数实现了带返回值的调用,这里实现完整的带参数-返回值的调用,来实现一个加法的功能。
汇编想要传递数据和上面实现返回值的思路是同样的,能够用不少种方式来考虑,好比使用数据段、使用栈、使用寄存器。
数据段通常不要用来作这种参数的传值,由于参数数据是临时,应该使用完成就释放掉,不该该存到数据段中。
在iOS中,编译器默认是优先使用寄存器传值的,当寄存器不够用时才会用栈,这里咱们先用寄存器实现一个加法:
assume cs:code, ds:data,
data segment
db 20H dup(0)
data ends
code segment
start:
mov ax, data
mov ds, ax
mov cx, 1111h,
mov dx, 2222h,
call sum1
mov ax, 4c00h
int 21h
sum1:
mov ax, cx
add ax, dx
ret
code ends
end start
复制代码
先讲1111h、2222h存入寄存器cx、dx中,再调用sum1将cx、dx相加返回到ax,上面已经提到在汇编中将值存入ax即返回值。
下面咱们再用栈来传递参数,假如调用call sum以前,先将1111h、2222h入栈,那么在调用call的时候,又会将call的下一条指令的偏移地址入栈,那么此时栈顶实际上是call的下一条指令的偏移地址。如何在sum中直接pop的话,会将call的下一条指令的偏移地址出栈,那么ret后就没法继续执行call以后的代码了。因此在sum中不可以进行出栈操做,而是要直接访问栈内的元素。那么可能会有疑问,栈不是只能访问栈顶吗?汇编中不是的,汇编没有编译器语法和API的限制,只要是内存,咱们都可以访问。
assume cs:code, ds:data, ss:stack
stack segment
db 20 dup(0)
stack ends
data segment
db 20 dup(0)
data ends
code segment
start:
mov ax, stack
mov ss, ax
mov ax, data
mov ds, ax
push 1111h
push 2222h
call sum
add sp, 4
mov ax, 4c00h
int 21h
sum:
mov bp, sp
mov ax, ss:[bp+2]
add ax, ss:[bp+4]
ret
code ends
end start
复制代码
代码解析:
sum:
mov bp, sp
mov ax, ss:[bp+2]
add ax, ss:[bp+4]
ret
复制代码
push 1111h
push 2222h
call sum
add sp, 4
复制代码
经过上面的操做,就能够明白为何说高级语言中方法的参数是临时变量,由于仍是函数的操做都是在栈中,方法结束后栈又恢复原来栈顶位置。这个恢复栈的操做就是栈平衡。
回收内存空间的误区
栈只是恢复了栈顶的位置,原来存入栈的值并无恢复成00,所谓的内存的回收并非将内存从新清零,只是再也不占用这块内存,之后须要使用内存的时候能够用新的值覆盖这块内存。若是之前面对高级语言中的内存回收,认为是将内存清空是不对的。
递归调用的问题
经过上面的汇编能够知道,方法的调用须要将参数和下一条汇编指令的偏移地址入栈,在方法结束才会对栈顶进行恢复。当递归调用出现时,每个方法都会在其内部进行调用另外一个方法,至关于在当前汇编方法的ret以前又调用call,这样栈就会无限的push而不会pop,当栈溢出后,就会发生错误致使崩溃。
另外即使不是无限的递归,若是函数之间的调用层级深到必定程度,使得栈空间溢出的话,仍然会形成严重的错误乃至崩溃。并且函数之间的调用也会额外的占用栈空间(内存),这也就是为何高级语言大多数状况下若是可以用循环解决问题的话,都尽可能不用递归的缘由之一。
汇编代码至关于:
assume cs:code, ds:data, ss:stack
stack segment
db 20H dup(0)
stack ends
data segment
db 20H dup(0)
data ends
code segment
start:
mov ax, stack
mov ss, ax
mov ax, data
mov ds, ax
push 1111h
push 2222h
call sum
add sp, 4
mov ax, 4c00h
int 21h
sum:
mov bp, sp
mov ax, ss:[bp+2]
add ax, ss:[bp+4]
push 1111h
push 2222h
call sum
add sp, 4
ret
code ends
end start
复制代码
以下图,递归调用栈内无限的push
咱们刚刚使用的栈平衡的方法就是外平栈,在函数调用后面对栈进行平衡。
push 1111h
push 2222h
call sum
add sp, 4
复制代码
还有一种栈平衡的方法,在函数的内部进行栈平衡操做:
push 1111h
push 2222h
call sum
mov ax, 4c00h
int 21h
sum:
mov bp, sp
mov ax, ss:[bp+2]
add ax, ss:[bp+4]
ret 4
复制代码
C++的函数在调用时,能够指定本身对应的汇编代码使用哪一种方法传递参数和使用哪一种栈平衡方式:
int __cdecl sum(int a, int b) {
return a + b;
}
复制代码
注:iOS开发中,在Xcode里边设置是无效的,规定就是使用第三种方式,并且会使用更多的寄存器传递参数,基本知足开发使用的函数所有使用寄存器传参。
咱们还一直没有在汇编函数中建立局部变量,对应的高级语言以下函数:
int sum(int a, int b) {
int c = 3;
int d = 4;
int e = c + d
return a + b + e;
}
int mian() {
sum(1, 2);
return 0;
}
复制代码
根据局部变量的特性:只可以在函数内部访问而且函数结束后要回收内存,这样仍是使用栈来实现局部变量。下面就来用汇编实现上面的逻辑:
assume cs:code, ds:data, ss:stack
stack segment
db 100 dup(0)
stack ends
data segment
db 100 dup(0)
data ends
code segment
start:
; 将ds、ss指向程序的数据段和栈段
mov ax, data
mov ds, ax
mov ax, stack
mov ss, ax
; 传入参数
push 1h
push 2h
; 调用方法
; 至关于 sum(1, 2);
call sum
; 程序结束
mov ax, 4c00h
int 21h
sum:
; 当前sp的值复制给bp,用于从栈中取值
mov bp, sp
; 将sp=sp - 10,扩容栈,bp指向栈扩容前栈顶
; 用于函数存放函数中建立的临时变量
sub sp, 10
; 在栈中存入临时变量
; 在栈扩容前的栈顶位置加入,即bp-2的位置
; 对应 int c = 3;
mov word ptr ss:[bp-2], 3h
; 在栈中存入另外一个临时变量
; 对应 int d = 4;
mov word ptr ss:[bp-4], 4h
; 将两个临时变量相加并压入栈中
; 至关于 e = c + d;
mov ax, ss:[bp-2]
add ax, ss:[bp-4]
mov ss:[bp-6], ax
; 将函数的两个参数、两个临时变量的和的临时变量相加,并返回结果
; return a + b + e;
mov ax, ss:[bp+2]
add ax, ss:[bp+4]
add ax, ss:[bp-6]
; 函数开始将sp上移了,而bp记录着上移前的位置
; 由于ret的时候,须要从栈顶获取下一条指令的偏移地址,如今栈顶ss:sp指向的是扩容后的位置
; 须要将sp恢复到上移以前的位置
mov sp, bp
; 将栈顶pop给IP,继续执行call sum 的下一条指令
; 栈平衡(内平栈)
ret 4
code ends
end start
复制代码
下面分析一下流程:
在调用push 01h以前,还有没有传递参数,如今栈是空的,sp=64H,栈定为:0710:0064。
执行call sum后,call下一条指令的偏移地址会入栈,当前IP=000E,call指令的长度为3,那么下一条指令的偏移地址为000E + 3 = 11H。
能够看到,执行call sum指令后,0011H入栈。当前SP=005E,BP=0000E。
接下来将SP=SP-10,能够看到栈容量扩充了10byte,BP指向SP以前的值,即栈顶原来的位置。
将两个临时变量三、4入栈,三、4就在运来栈顶的位置继续压入,中间没有空余的内存浪费。
要将输入存入栈最后一个可用位置,加到SS:[BP-2]的位置正好是SP下移以前的位置。第二次临时变量继续在原来BP-2的基础再减2,就是SS:[BP-4],同理,当须要继续加入临时变量,就是SS:[BP-6]的位置。
将3和4的和7入栈,由于它对应的也是一个临时变量 e ,须要入栈保存。
将两个参数和两个临时变量的和相加,SS:[BP]指向的位置是0710:005E,以前两个参数0001H、0002H存放的位置是:SS:[BP+2]、SS:[BP+4]的位置,临时变量的和存放在SS:[BP-6]的位置。能够发现一个规律,参数同BP+X获取,临时变量经过BP-X获取。
将相加结果存入ax寄存器中,ax=0001H+0002H+0003H+0004H=000A,结果复合预期。
ret以前须要现将以前为了存放临时变量将SP=SP-10恢复,这里只须要将BP的值给SP就能够了,能够看到临时变量0003H、0004H、0007H都释放了。
执行ret 4,将栈顶0011E给IP,CS:IP指向call sum的下一条指令继续执行接下来的汇编代码,而后内平栈,能够发现栈顶恢复到函数函数以前的位置0710:0064,函数调用占用的全部栈空间所有回收。
上面的代码看上去好像没有任何问题了,可是实际上还不够,只是单个函数调用的时候看上去能够的。下面假设以下的代码:
int minus(int a, int b) {
int c = 1;
int d = 2;
int e = a - b;
return e + c - d;
}
int sum(int a, int b) {
int c = 3;
int d = 4;
// 0 + 1 + 2 + 3 + 4 = 10
int e = minus(8, 7);
// 0 +
return e + a + b + c + d;
}
int mian() {
// 000A;
sum(1, 2);
return 0;
}
复制代码
按照以前单个函数调用的方式,在添加一个minus方法,并在sum函数内部调用它:
assume cs:code, ds:data, ss:stack
stack segment
db 100 dup(0)
stack ends
data segment
db 100 dup(0)
data ends
code segment
start:
mov ax, data
mov ds, ax
mov ax, stack
mov ss, ax
push 2h
push 1h
call sum
mov ax, 4c00h
int 21h
sum:
mov bp, sp
sub sp, 10
mov word ptr ss:[bp-2], 3h
mov word ptr ss:[bp-4], 4h
push 7h
push 8h
call minus
mov word ptr ss:[bp-6], ax
mov ax, ss:[bp+2]
add ax, ss:[bp+4]
add ax, ss:[bp-2]
add ax, ss:[bp-4]
mov sp, bp
ret 4
minus:
mov bp, sp
sub sp, 10
mov word ptr ss:[bp-2], 1h
mov word ptr ss:[bp-4], 2h
mov ax, ss:[bp+2]
sub ax, ss:[bp+4]
mov ss:[bp-6], ax
mov ax, ss:[bp-6]
add ax, ss:[bp-2]
sub ax, ss:[bp-4]
mov sp, bp
ret 4
code ends
end start
复制代码
运行起来咱们会发现汇编指令无限的进行一个循环操做,根本没法正常结束。如今来分析一下缘由:
调用call minus这里都是咱们前面都已经理解的流程,下面开始是关键之处。
call minus以前首先将0007H和0008H入栈,如今SP由0054变成了0050,注意当前BP的值是sum栈扩容前SP的值,BP是关键,要特别关注。
这里再额外注意一下call minus 的下一条指令和它的指令地址:
; 071E:0034
mov word ptr ss:[bp-6], ax
复制代码
当执行call minus后,栈中又压入call minus后面指令的偏移地址,上面能够知道call minus指令的偏移地址是0031,长度是3,因此0031+3=0034H,入栈的值也恰好是0034H。如今BP的值依然是005E。
接下来要执行的代码就是 mov bp, sp 。这个方法以前单独执行sum方法的时候已经使用过,是为了可以访问当前方法使用的栈的元素,并且也是为了释放当前栈的局部变量空间。
如今BP已经从以前的005E指向了当前SP的值004E了,并且当前SS:BP存储的值是0034,是sum函数中 call minus后面一条指令的偏移地址。
当minus执行完计算结果到 mov sp, bp这条指令时;ax=8-7+1-2=0H,结果复合预期。当前SP指向0044,BP指向004E,下面执行mov sp, bp,将bp的值送给sp。
接下来minus执行ret,将0034送给IP,并将恢复栈平衡。
虽然如今会直接执行call minus后面的代码:
; 071E:0034
mov word ptr ss:[bp-6], ax
复制代码
这里已经发现了问题:BP仍是指向004E,这就致使sum内部在调用minus函数以后,经过bp取的值都是错误,并且sum在结算完结果后,恢复sp的时候,使用的bp也是错误的,它会将sp设置为0034。
而ret指令又将ip设置为ss:sp的值,即ip变成了0034,这就致使ret以后执行的不是call sum以后的指令,而是071E:0034地址对应的指令。
这个指令咱们以前已经特别记录了,就是sum函数中调用call minus后面的指令,因此sum在调用call minus后,会在 mov word ptr ss:[bp-6], ax 和 ret 4 之间无限的循环执行。
既然问题出如今BP寄存器,咱们就须要想办法解决汇编函数之间调用,BP寄存器的状态恢复问题。
上面已经分析了函数之间调用是因为BP的问题,这里就来解决这个问题,依然是使用栈来对BP的原始值进行存储,在须要恢复的BP的时候进行恢复,只须要添加和修改几处代码:
assume cs:code, ds:data, ss:stack
stack segment
db 100 dup(0)
stack ends
data segment
db 100 dup(0)
data ends
code segment
start:
mov ax, data
mov ds, ax
mov ax, stack
mov ss, ax
; 记得前面的函数调用约定吧,参数从右到左入栈
push 2h
push 1h
call sum
mov ax, 4c00h
int 21h
sum:
; 先将bp的值入栈存储,函数结束将bp恢复
; 由于这里有了一次push操做,因此当前栈顶相比以前多了bp的值
; 如今栈顶数据结构为:
; -- bp值
; -- 函数调用完要执行的指令的偏移地址
; -- 函数的参数
push bp
; 将sp的值赋值给bp
; 由于栈相比以前多push了bp的原始值
; 因此此时bp的地址相比以前要小2
mov bp, sp
sub sp, 10
mov word ptr ss:[bp-2], 3h
mov word ptr ss:[bp-4], 4h
push 7h
push 8h
call minus
mov word ptr ss:[bp-6], ax
; 由于bp比以前小2,因此经过bp取参数须要在原来的基础上+2
mov ax, ss:[bp+4]
add ax, ss:[bp+6]
add ax, ss:[bp-2]
add ax, ss:[bp-4]
; 将bp赋值给sp以后,这里的sp相比以前+2
; 栈顶存放值bp以前的值
mov sp, bp
; 将bp原来的值从新送给bp
; 如今栈顶从新指向了下一条指令的偏移地址
pop bp
ret 4
minus:
push bp
mov bp, sp
sub sp, 10
mov word ptr ss:[bp-2], 1h
mov word ptr ss:[bp-4], 2h
mov ax, ss:[bp+4]
sub ax, ss:[bp+6]
mov ss:[bp-6], ax
mov ax, ss:[bp-6]
add ax, ss:[bp-2]
sub ax, ss:[bp-4]
mov sp, bp
pop bp
ret 4
code ends
end start
复制代码
观察ax寄存器中,已经获得正确的结果000AH,BP按照上图的流程正确的恢复为0000E,栈空间也正确的回收。
经过上面的函数调用应该已经能够发现汇编的规律:
将上面的C++代码运行并查看其对应的汇编代码:
13: int sum(int a, int b) {
// 对应咱们汇编代码的 push bp
00401080 push ebp
// 将sp值赋给bp
00401081 mov ebp,esp
// 栈顶下移扩容栈空间,用于存放临时变量
00401083 sub esp,4Ch
00401086 push ebx
00401087 push esi
00401088 push edi
// 填充栈空间
00401089 lea edi,[ebp-4Ch]
0040108C mov ecx,13h
00401091 mov eax,0CCCCCCCCh
00401096 rep stos dword ptr [edi]
// 局部变量 经过 bp-x 赋值
14: int c = 3;
00401098 mov dword ptr [ebp-4],3
15: int d = 4;
0040109F mov dword ptr [ebp-8],4
16: // 0 + 1 + 2 + 3 + 4 = 10
17: int e = minus(8, 7);
// 调用函数前经过栈push参数
// 从右到左入栈
004010A6 push 7
004010A8 push 8
004010AA call @ILT+10(minus) (0040100f)
004010AF add esp,8
004010B2 mov dword ptr [ebp-0Ch],eax
18: // 0 +
19: return e + a + b + c + d;
004010B5 mov eax,dword ptr [ebp-0Ch]
004010B8 add eax,dword ptr [ebp+8]
004010BB add eax,dword ptr [ebp+0Ch]
004010BE add eax,dword ptr [ebp-4]
004010C1 add eax,dword ptr [ebp-8]
20: }
004010C4 pop edi
004010C5 pop esi
004010C6 pop ebx
004010C7 add esp,4Ch
004010CA cmp ebp,esp
004010CC call __chkesp (00401140)
// 将bp值给sp
004010D1 mov esp,ebp
// 恢复bp
004010D3 pop ebp
004010D4 ret
复制代码
虽然在函数内部穿插着不少如今还看不懂的代码,可是仍是可以从中发现,函数调用的整体流程是一致的。下面就来一一去解释和上面比咱们多出来的代码指令。
这段代码是在保护和恢复bp寄存器的基础上,对以下寄存器也进行了保护和恢复:
// 栈顶下移扩容栈空间,用于存放临时变量
00401083 sub esp,4Ch
00401086 push ebx
00401087 push esi
00401088 push edi
004010C4 pop edi
004010C5 pop esi
004010C6 pop ebx
复制代码
下面这段代码是将栈空内用于存放临时变量的空间,所有用CC填充,当用程序异常,IP指向了临时变量的值,而且这里的值是CC的话,就会中断,程序停在这里,是一种安全机制。
00401089 lea edi,[ebp-4Ch]
0040108C mov ecx,13h
00401091 mov eax,0CCCCCCCCh
00401096 rep stos dword ptr [edi]
复制代码
咱们也把其余寄存器的保护代码加上,实现一个完整的汇编程序:
; 代码段、数据段、栈段声明
assume cs:code, ds:data, ss:stack
; 栈段
stack segment
; 定义栈容量
db 100 dup(0)
stack ends
; 数据段
data segment
db 100 dup(0)
data ends
; 代码段
code segment
; 程序入口
start:
; 数据段关联
mov ax, data
mov ds, ax
; 栈段关联
mov ax, stack
mov ss, ax
; 记得前面的函数调用约定吧,参数从右到左入栈
; 参数入栈
push 2h
push 1h
; 调用函数
call sum
; 程序正常退出
mov ax, 4c00h
int 21h
sum:
; 先将bp的值入栈存储,函数结束将bp恢复
; 由于这里有了一次push操做,因此当前栈顶相比以前多了bp的值
; 如今栈顶数据结构为:
; -- bp值
; -- 函数调用完要执行的指令的偏移地址
; -- 函数的参数
push bp
; 将sp的值赋值给bp
; 由于栈相比以前多push了bp的原始值
; 因此此时bp的地址相比以前要小2
mov bp, sp
sub sp, 10
; 在后面保护寄存器
; 由于这样方便bp访问局部变量和参数,让bp在局部变量和参数之间
push bx
push si
push di
; 函数执行逻辑
mov word ptr ss:[bp-2], 3h
mov word ptr ss:[bp-4], 4h
; 传入参数
push 7h
push 8h
; 调用函数
call minus
; 函数执行逻辑
mov word ptr ss:[bp-6], ax
; 由于bp比以前小2,因此经过bp取参数须要在原来的基础上+2
mov ax, ss:[bp+4]
add ax, ss:[bp+6]
add ax, ss:[bp-2]
add ax, ss:[bp-4]
; 恢复寄存器
pop di
pop si
pop bx
; 将bp赋值给sp以后,这里的sp相比以前+2
; 栈顶存放值bp以前的值
mov sp, bp
; 将bp原来的值从新送给bp
; 如今栈顶从新指向了下一条指令的偏移地址
pop bp
; 结束函数并恢复栈平衡
ret 4
minus:
push bp
mov bp, sp
sub sp, 10
push bx
push si
push di
mov word ptr ss:[bp-2], 1h
mov word ptr ss:[bp-4], 2h
mov ax, ss:[bp+4]
sub ax, ss:[bp+6]
mov ss:[bp-6], ax
mov ax, ss:[bp-6]
add ax, ss:[bp-2]
sub ax, ss:[bp-4]
pop di
pop si
pop bx
mov sp, bp
pop bp
ret 4
code ends
end start
复制代码
完整的汇编函数调用流程:
1,push 参数
传递参数给函数
2,push 函数下一条指令的偏移地址
用于函数执行完成后,可以正确执行后面的指令
3,push bp
保存bp以前的值用于恢复bp
4,mov bp, sp
保留sp以前的值,用于恢复sp;用于访问栈空间的数据
5,sub sp x
分配栈空间给函数用于存储局部变量,x为自定义的大小
6,push bx\si\di
保护须要保护的寄存器
7,执行函数代码
8,pop di\si\bx
恢复寄存器
9,mov sp,bp
恢复sp
10,pop bp
恢复bp
11,ret (x)
函数返回,栈平衡(内平栈或外平栈)
复制代码
经过8086汇编了解汇编的基础以后,就能够很容易的理解和读懂AT&T汇编了,这是iOS虚拟机中使用的汇编。