从零入门8086汇编

为何要了解汇编?

了解汇编语言可以更加深刻的理解高级语言的本质,完全理解以前只是知道却又不清楚为何的知识,好比:程序员

  • 程序内存分段的原理,为何有代码段、数据段、堆栈段?
  • 全局变量为何在程序运行就已经建立好了?
  • 为何递归比循环效率低?
  • 为何无限递归程序会崩溃?
  • 编译器断点是如何实现的?
  • 局部变量的内存是如何回收的?
  • 内存回收的本质是什么?
  • 等等等...

基本概念

汇编语言的种类

  • 8086汇编(8086处理器是16bit的CPU)
  • Win32汇编
  • Win64汇编
  • AT&T汇编(Mac、iOS模拟器)
  • ARM汇编(嵌入式、iOS真机)

高级语言的编译流程

  • 高级语言 编译为 汇编语言
  • 汇编语言 编译为 机器语言
  • 机器语言 运行在计算机上

编译和反编译

  • 汇编语言和机器语言一一对应,每一条机器指令都有对应的汇编指令。
  • 汇编语言能够编程为机器语言,机器语言能够反汇编获得汇编语言。
  • 高级语言能够编译为汇编语言\机器语言,汇编语言\机器语言没法还原成高级语言。

查看高级语言对应的汇编

使用 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。经过汇编语言看逆向推导,是没法知道高级语言是建立了数组仍是结构体的。这就是为何汇编语言没法还原成高级语言。数据结构

sizeof 是函数调用吗?

经过查看对应的汇编代码能够看到:架构

直接将4传给对应的内存空间,并无调用任何函数,因此sizeof并非一个函数,而是一个编译器特性,在编译后直接转成了对应的汇编代码。函数

同理在Xcode中也是同样的:工具

总线

总线:即一根根导线的集合。

每个CPU新片都有许多管脚,这些管脚和总线相连,CPU经过总线和外部器件进行交互。

总线的分类:

  • 地址总线:寻址,找到地址对应的存储空间
  • 数据总线:传递,CPU和内存之间传递具体数据
  • 控制总线:控制,高度内存须要进行读仍是写操做

CPU从内存中读取数据的步骤:

  1. 首先CPU经过地址线 找到要读取数据地址
  2. 经过 控制线 告诉内存去要 读取 操做
  3. 内存经过 数据线 返回数据给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内存空间的示意图:

  • 0x00000 - 0x9FFFF:主存储空间,可读可写
  • 0xA0000 - 0xBFFFF:显存地址空间,数据显示在显示器上
  • 0xC0000 - 0xFFFFF:ROM,只读

8086的寻址方式

上面提到8086的地址总线宽度为20,寻址能力为1M,可是实际上8086是一个16位架构的CPU,它内部可以一次性处理、传输、暂存的数据只有16位。这就意味这8086实际上只可以直接送出16的地址,可是它的地址总线宽度又是20位,意味这这样就有4位是没法使用的,它的实际寻址能力只可以是64KB。那么它是如何作到实现1M的寻址能力呢,具体步骤以下:

  1. CPU中的相关部件提供两个16的地址,一个成为段地址,一个成为偏移地址。
  2. 段地址和偏移地址经过内部总线送入地址加法器。
  3. 地址加法器将两个16位地址合成一个20位的物理地址。
  4. 地址加法器经过内部总线将20位物理地址送入输入输出控制电路。
  5. 输入输出控制电路将20位物理地址送入地址总线。
  6. 20位的物理地址被地址总线送到内存。

段地址和偏移地址合成物理地址的计算规则:物理地址 = 段地址 * 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在对内存中的数据进行运算时,首先将内存中的数据存储到寄存器中,而后再对寄存器的数据进行运算。

字节与字

汇编语言没有数据类型的概念,它是直接操做内存的,汇编语言的数据存储单位有两个:

  1. 字节:byte,1个字节由8bit组成,能够存储在8位寄存器中。
  2. 字:word,1个字由2个字节组成,这两个字节分别成高字节和低字节。
好比数据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 代码段寄存器

CS和IP配合使用,它们指示了CPU当前要读取指令的地址。任什么时候候,8086CPU都会将CS:IP指向的指令作为下一条须要取出执行的指令。

指令执行的过程:

  1. 从CS:IP指向的代码段内存单元读取指令,读取的指令进入指令缓冲器。
  2. IP = IP+读取指令的长度,进而能够读取下一条指令。
  3. 返回步骤1。

在内存或者磁盘上中,指令和数据没有任何区别,都是二进制信息。 CPU在工做时,有时候把信息看成指令,有时候看做数据,一样的信息赋予不一样的意义。

CPU根据什么将内存中的数据信息看成指令? 经过CS:IP指向的内存单元内容看做指令。

DS 数据段寄存器

DS是用来操做内存时提供段地址的,假如须要将内存中10000H 存入1122H,直接这样写是不能够的:

mov 1000H:[0H],1122H
复制代码

由于汇编语言又以下要求:

  1. 不能直接给内存地址赋值,必须经过DS:[偏移地址]指向内存。
  2. 不能直接经过给DS赋值,必须经过寄存器中转。
; 不能直接给DS赋值,须要经过寄存器中转
mov ax, 1000H
mov ds, ax
; 不能直接给内存地址赋值,必须经过DS:[偏移地址]指向内存
; 内存中的10000H位置存入了1122H
mov [0H], 1122H
复制代码

SS 堆栈段寄存器

SS配合SP使用,SS:SP指向栈顶元素。后面栈章节中会有更详细的介绍。

8086经常使用指令

mov指令

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
复制代码

jmp指令

在高级语言中,不少状况下都须要改变代码的执行流程,好比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是汇编语言中加法操做,add ax, 1111H 指令为将寄存器ax中的值加上1111H再赋值给ax。

; ax=1122H
mov ax,1122H
; ax=2233H
add ax,1111H
复制代码

sub指令

sub是汇编语言中减法操做,sub ax,0011H 指令为将寄存器ax中的值减去0011H再赋值给ax。

; ax=1122H
mov ax,1122H
; ax=1111H
sub ax,0011H
复制代码

push指令

入栈,详见后面栈章节。

pop指令

出栈,详见后面栈章节。

大小端

  • 大端模式:数据的高字节保存在内存的低地址中,数据的低字节保存在内存的高地址。(Big Endian)
  • 小端模式:数据的高字节保存在内存的高地址中,数据的低字节保存在内存的低地址。(Little Endian)
将0x1234存放在CPU内存中的0x4000位置,大小端的区别为:
            小端            大端
0x4000      0x34           0x12
0x4001      0x12           0x34
复制代码

小端模式:808六、x86

大端模式:PowerPC、IBM、Sun

ARM既能够工做在大端模式,也能够工做在小端模式

8086汇编环境使用和调试

须要运行和调试8086汇编会好的工具就是这个软件 emu8086,这个软件能够很是方便和直观编写、调试、运行8086汇编,支持Windows平台,软件界面以下:

安装完成后我先尝试使用一下:

打开emu8086,打开后默认就有一个编辑界面,咱们尝试在内存中10003H中写入1234H,编写以下指令后点击emulate按钮执行:

执行后会弹出一个调试窗口,点击窗口顶部的菜单栏view-memory打开内存查看视图:

在内存查看视图修改默认的段地址和偏移地址,查看1000:0000的位置,能够看到内存中1000:0003位置的值都是00H

如今观察调试窗口的信息

左侧是当前全部寄存器的值;中间蓝色的是当前执行指令的位置,蓝色的行数就是当前执行指令的长度;右侧就是当前即将执行的指令。咱们能够发现以下规律:

  • CS=0100H,IP=0000H,CS:IP=0100H*10H+0000H=01000H,恰好就是当前执行的指令的地址。
  • 当前AH=00H,AL=00H。
  • DS=0100。

点击single step执行mov ax, 1000H

  • AH=10H,AL=00H,1000已经成功存入AX中。
  • IP=0003,恰好是增长了3,即上一条执行指令的长度。
  • 当前的CS:IP恰好是如今即将执行的指令mov ds, ax的地址,该指令长度为2。
  • DS=0100。

点击single step执行mov ds, ax

  • DS=1000H,成功的将ax的值传给了DS。
  • IP=0005H,恰好是0003H加上上一条指令的长度2。
  • CS:IP恰好指向即将执行的指令:mov bx, 1234H,该指令的长度为3。

点击single step执行mov bx, 1234H

  • BX=1234H。
  • IP=0008H,恰好是0005H加上上一条指令的长度3。
  • CS:IP恰好执行即将执行的指令:mov [3H], bx,该指令长度为4。

点击single step执行:mov [3H], bx

  • IP=0008H+4=000CH
  • CS:IP=0100CH,等于一下条指令的地址。
  • DS:[3H]=1000:0003,该位置的内存的值已经成功赋值1234H。

栈是一种具备特殊访问方式的存储空间(后进先出),在栈和队列中有关于栈的数据结构和原理介绍。

  • 8086会将CS做为代码段的段地址,将CS:IP指向的指令做为下一条须要取出执行的指令。
  • 8086会将DS做为数据段的段地址,mov ax,[address]就是将DS:address对应的内存数据放到ax寄存器中(放入不一样于移动,DS:address地址的值不会清0)。
  • 8086会将SS做为栈段的段地址,SS:SP指向栈定元素。
  • 8086提供PUSH和POP指令来操做堆栈段的数据。
; 将ax寄存器的数据入栈
push ax
; 将栈顶的数据送入ax寄存器
pop ax

; 注:8086 push和pop就是以word为单位,没有byte的操做,不须要指定单位
复制代码

如今假设SS=1000H,SP=0004H,AX寄存器中存放着2266H,而且如今栈的内存空间都是存放00H。

下面就是栈的当前内存结构:

push

push ax 指令执行的步骤:

  • 首先要将SP=SP-2,栈顶指针上移,即SS:SP的值也减小了2。
  • 将ax寄存器的值存入内存中栈顶的位置,即SS:SP的位置。

虽然栈顶相对内存是上移的,可是存入两个字节时,仍是要从栈顶往高拿两个字节的内存存放元素。

如上存入2266H,栈顶上移两位后为:10002H,那么须要用10002H和10003H存放2266H。8086是小端模式,高字节22H放在高地址10003H,低字节66H放在低地址10002H。

pop

接着上面的栈的状态,咱们如今执行指令 pop bx。

  • 将SS:SP指向的内存地址取两个字节给bx寄存器存储。
  • SP=SP+2,栈顶指针下移,即SS:SP的值增长了2。

:先从栈顶指针指向的内存位置取两个字节的数据,依然是往高取两个字节: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类:

  • 汇编指令:如mov、add、sub等
    • 由对应的机器指令,能够被编译为机器指令,最终由CPU执行
  • 伪指令:如assume、segment、ends、end等
    • 没有对应的机器指令,由编译器解析,最终不被CPU执行

segment和ends

上面的segment和ends的做用是定义一个段,segment表明段的开始,ends表明段的结束:

段名 segment
    :
段名 ends
复制代码

一个有意义的汇编程序中,至少要有一个段作为代码段存放代码。

assume

assume 的做用是将代码段和mycode段和CPU中的CS寄存器关联起来。

end

end 代码程序的结束,编译器遇到end就会结束编译。

中断 (重要)

下面的代码表明退出程序,int不是整形的意思,是interrupt的简写,表明中断:

; 只要ah是4ch就能够结束
; al是返回码,相似于return 0的0,mov ax, 4c00h
mov ah, 4ch 
int 21h
复制代码

中断是因为软件或硬件的信号,使CPU暂停执行当前的任务,转而去执行另外一段子程序。

  • 硬中断(外中断):由外部设备,如网卡、硬盘随机引起的,好比网卡收到数据包的时候,就会发出一个中断。
  • 软中断(内中断):由执行中的指令产生的,能够经过程序控制触发。

能够经过 “ int 中断码 ” 实现中断,内存中有一张中断向量表,用来存放中断码处理中断程序的入口地址。CPU在接受到中断信号后,暂停当前正在执行的程序,跳转到中断码对应的向量表地址处去执行中断。

常见中断:

  • int 10h 用于执行BIOS中断
  • int 3 断点中断,用于调试程序
  • int 21h 用于执行DOS系统功能调用,AH寄存器存储功能号。

下面是int21对应AH寄存器部分功能对照表

AH 功能 调用参数 返回参数
09 显示字符串 DS:DX=串地址 (DS:DX+1)=实际输入的字符数
4c 带返回码结束 AL=返回码 无返回参数

db、dw

db基本使用

汇编语言中可使用db定义数据:

; 定义100个
// 定义一个字节的00H
db 0h
// 定义一个字的数据0000H
dw 0h
复制代码

在数据段定义数据至关于建立全局变量

在栈段定义数据至关于指定栈的容量

在代码段定义数据通常不会这样使用

dup 批量声明

可使用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内存管理的话必定知道下面的iOS内存布局:

低地址
  |            保留段
  |    
  |            代码段(__TEXT)
  |         
  |            数据段(__DATA)
  |                 字符串常量
  |                 已初始化数据:已初始化的全景变量、静态变量等
  |                 未初始化数据:未初始化的全局变量、静态变量等
  |
  |            堆(heap)⬇️ 地址愈来愈高
  |
  |            栈(stack)⬆️地址愈来愈低
  |
  |            内核区
高地址
复制代码

首先看一下是否是和咱们刚刚实现的汇编很类似,有代码段和和数据段。

其中栈地址愈来愈低是否是和咱们刚刚汇编分析的栈push同样,push的时候SP=SP-2,栈顶指针上移,栈顶指针变小,地址变低。

全局变量放在数据段中,即程序运行时就将数据放在数据段中跟刚刚汇编代码中在数据段建立数据是同样的,还有这也解释了为何全局变量的地址在编译就已经肯定好不会再次改变,咱们汇编中在数据段建立的数据地址也是肯定而且不会变化的。

HelloWorld输出

有了上面的基础指令和分段以后,终于能够实现经典程序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 汇编中的函数调用

使用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
  • IP=0005,CS:IP=call 0000Ch的地址
  • 栈为空
  • 当前指令的长度为3,下一条指令的IP应该为0712:0008

下面执行call 0000Ch

  • 栈中存入了0008H,正好是上一步里边call 0000Ch下一条指令的地址
  • 当前指令的地址是0712C

下面一直执行到ret

  • 栈中的0008H出栈,而且赋值给了IP
  • CS:IP恰好是call以后应该继续执行的指令的地址

经过上面的分析能够知道call和ret的做用

call

将下一条指令的偏移地址入栈

执行函数

ret

将栈顶的值出栈,赋值给IP

带返回值的函数

上面咱们经过call print实现了打印hello world,这里咱们换成另外一种方式,让call print返回须要打印的字符串的偏移地址,ret后打印出来。首先就是考虑如何将字符串的编译地址返回出来。

  • 用栈能够吗?若是用栈的话,即print中将hello的偏移地址入栈,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
复制代码
  • 用寄存器?寄存器固然能够,并且也是大多数状况默认的作法,并且大多数都是将返回值放在ax寄存器中。
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
复制代码

验证函数的返回值存放在ax寄存器中:

先执行函数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
复制代码
  • 能够经过ss:[偏移地址]直接获取栈空间的值。
  • 8086汇编不容许直接经过[sp]取值,因此须要将sp的值先传给bp。
  • 当前栈顶ss:sp指向的是call sum下一条指令的偏移地址
  • ss:[sp+2]就是2222h
  • ss:[sp+4]就是1111h
  • 现将2222h给ax,而后再将1111h加到ax中就实现了加法
push 1111h
    push 2222h
    call sum   
    add sp, 4
复制代码
  • 先经过两次入栈将参数压入栈
  • call sum将下一条指令的偏移地址压入栈
  • 栈一共进行了3次push操做
  • sum内部只有在ret的时候将call sum下一条指令的偏移地址出栈
  • call sum执行结束后,栈只在ret时进行了一次pop操做
  • 为了恢复栈空间,须要将sp=sp+4,至关于两次pop操做对sp的移动
  • 由于call sum后,只须要恢复栈顶位置,并不须要取值,因此直接移动sp就能够了,在汇编中都是一样的操做。

栈平衡

经过上面的操做,就能够明白为何说高级语言中方法的参数是临时变量,由于仍是函数的操做都是在栈中,方法结束后栈又恢复原来栈顶位置。这个恢复栈的操做就是栈平衡。

回收内存空间的误区

栈只是恢复了栈顶的位置,原来存入栈的值并无恢复成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++的函数在调用时,能够指定本身对应的汇编代码使用哪一种方法传递参数和使用哪一种栈平衡方式:

  • __cdecl:外平栈,参数从右到左入栈
  • __stdcall:内平栈,参数从右到左入栈
  • __fastcall:内平栈,ecx、edx分别传递前面两个参数,其他参数从右到左入栈。
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的原始值进行存储,在须要恢复的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,栈空间也正确的回收。

经过上面的函数调用应该已经能够发现汇编的规律:

  • 经过ss:[bp-x] 获取的是函数的局部变量
  • 经过ss:[bp+x] 获取的是函数的参数

其余寄存器的保护和恢复

将上面的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虚拟机中使用的汇编。

相关文章
相关标签/搜索