汇编语言初认识

1. 函数的内存布局

重要声明:了解函数的内存布局对于程序的认识有很大的帮助,这种认识需要从汇编的角度窥探。下面给出学习链接:https://ke.qq.com/webcourse/index.html#cid=348781&term_id=100414803&taid=2833024853299821&vid=e1430bkk6ij

要被执行的代码(机器指令)本身是放在代码区的,我们也知道,程序的运行本质上就是处理数据的过程,并且这些数据是一直在变化,所以有必要在栈空间开辟一段专属于该函数的区域,用于存放该函数的局部变量。这便是代码区和栈空间的关系。

代码区是只读空间。

调用一个函数,会开辟一段栈空间给函数

寄存器:ESP,EBP,栈指针寄存器

ESP:stack pointer,栈顶指针寄存器

EBP:栈空间的基地址指针寄存器。并且每一个函数的局部变量和形式参数都是以当前函数的基地址指针EBP作为参考进行初始化、赋值等操作,所以EBP非常关键


内存对齐基本概念

代码区 栈空间

语法糖 INT3 == CC 函数栈空间被当作代码来执行函数栈空间被当作代码区来执行

2. 内存空间的布局

每个应用都有自己独立的内存空间,其内存空间一般都有一下几大区域:

■ 代码区(代码段):用于存放代码

■ 数据段(全局区):用于存放全局变量等

■ 栈空间:每调用一个函数就会给他分配一段连续的栈空间,等函数调用完毕后会自动回收这段栈空间。自动分配和回收

■ 堆空间:需要主动申请和释放

堆空间

在程序运行过程中,为了能够自由控制内存的声明周期、大小,会经常使用堆空间的内存

memset

全局区的变量默认值都是零。

3. CS(code segment)

8086CPU外部的地址总线是20根,但cpu内部的地址总线(内部地址总线)是16位。所以每次寻址时,CPU会将两个内部的16位地址数据进行合并,得到一个宽度为20的地址,从而将该地址信号传送到外部的地址总线上。两个16位宽度的内部地址分别称为:段地址,偏移地址。这个概念非常重要。

  1. 段地址在8086CPU的段寄存器中存放。当8086CPU访问内存时,由段寄存器提供段内存单元的段地址。8086CPU有4个段寄存器,其中CS用来存放指令的段地址。
  2. CS用于存放指令的段地址,IP存放指令的偏移地址。

8086CPU,任意时刻,CPU将CS:IP指向的内容当作指令执行。

4. DEBUG指令

  • R命令查看改变CPU寄存器的内容
  • D命令查看内存中的内容
  • E命令改写内存中的内容
  • A命令以汇编指令的格式在内存写入一条机器指令。
  • U命令将内存中的机器指令翻译成汇编指令;
  • T 执行 当前 CS:IP 所指向的机器指令

第三章

3.1 内存中字的存储

3.2 DS和[address]

  • 在8086PC中,内存地址由段地址偏移地址组成。
  • 8086 CPU中有一个DS寄存器(数据段寄存器),通常用来存放要访问的数据的段地址。

mov指令可完成的两种传送功能:
(1) 将数据直接送入寄存器:mov ax, 2
(2) 将寄存器中的内容送入到另一个寄存器中:mov bx, ax

还可以将一个内存单元中的内容送入到一个寄存器
mov al [0]
注意:中括号[]中存放的是内存地址;mov 指令不能直接对内存操作,mov操作的其中一方必须要有寄存器或直接数(如,3,5)
例如:我们要读取10000H单元的内容可以用如下程序段进行:

mov bx, 1000H
mov ds, bx
mov al, [0]  // 注意,[]中的数字表示内存单元的偏移地址,而内存单元的段地址就是DS

上面三条指令将10000H (1000:0) 中的数据读到 al 中。

执行指令时,8086CPU自动取DS中的数据为内存单元的段地址。

如何用mov指令从10000H 中读取数据?

  • 10000H 表示为 1000:0(段地址:偏移地址)
  • 将段地址1000H 放入ds
  • mov al,[0]完成传送

注意,8086CPU不支持将数据直接送入段寄存器的操作,ds是一个段寄存器。这是硬件设计的问题。

  • mov ds, 1000H是非法的。

正确的操作是:

  • 数据→通用寄存器→段寄存器

分析问题本质:
怎样将数据从寄存器送入到内存单元?

结论:

mov bx, 1000H
mov ds, bx
mov [0], al  // 大概就是这么玩的

3.3 字的传送

因为8086CPU是16位结构,有16根数据线,所以,可以一次性传送16位的数据,也就是一次性传送一个字。

比如:

mov bx, 1000H
mov ds, bx
mov ax, [0]  // 注意,这里写的是ax,而不是ah或al,所以这里会将1000:0处的“字”型数据送入到ax
mov [0], cx  // cx中的16位数据送到1000:0处

3.4 mov, add, sub指令

add和sub指令同mov一样,都有两个操作数对象。

那么问题老了,这些指令可以对段寄存器进行add,sub操作吗?
答案是不行。不可以对段寄存器进行add, sub操作,其实对段寄存器进行加减操作也没有意义。

总结:

  1. 字在内存中存放时,要用两个地址连续的内存单元来存放,字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。
  2. 用mov指令要访问内存单元,可以在mov指令中只给出单元的偏移地址,此时,段地址默认是在DS寄存器中。
  3. [address]表示一个偏移地址为address的内存单元。
  4. 在内存和寄存器之间传送数据是,高地址单元和高8位寄存器、低地址单元和低8位寄存器相对应。
  5. mov、add、sub是具有两个操作对象的指令。jmp是具有一个操作对象的指令。
3.5 栈

3.5.1 CPU提供的栈机制

现今的CPU都有栈的设计
8086CPU提供相关的指令来以栈的方式访问内存空间。
这意味着,我们在基于8086CPU编程的时候,可以将一段内存当作栈来使用。

  • 8086CPU提供入栈和出栈指令:
    • PUSH(入栈)
    • POP (出栈)

PUSH ax:将寄存器ax中的数据送入栈中;
POP ax:从栈顶取出数据送入ax。

  • 8086CPU的入栈和出栈操作都是以为单位进行的。

3.5.2 两个疑问

  1. CPU如何知道一段内存空间被当作栈使用?
  2. 执行push和pop操作的时候,如何知道那个单元是栈顶单元?

CPU如何知道当前要执行的指令所在的位置?
答:代码段寄存器CS和指令指针寄存器IP中存放着当前指令的段地址和偏移地址。

8086CPU中,有两个寄存器和栈操作有关:

  • 段寄存器SS(Stack Segment): 存放栈顶的段地址
  • 栈指针寄存器SP(Stack Pointer): 存放栈顶的偏移地址

任意时刻,SS:SP指向栈顶元素

PUSH指令的执行过程:
在这里插入图片描述

注意

  • push时候,先进性SP(栈指针寄存器)的SP = SP - 2操作,只有这样才能腾出空闲的字空间存入外部数据。
  • pop操作正好相反,先将当前的SS:SP所指向的栈顶元素读走,再进行SP = SP + 2操作。
  • 栈的填充是从高地址低地址方向延申的。

在这里插入图片描述

3.6 栈顶越界的问题

8086CPU的工作机理,只考虑当前的情况:

  • 当前栈顶在何处。
  • 当前要执行的指令是哪一条。
mov ax, 1000H
mov ss, ax
mov sp, 0010H
mov ax, 002AH
mov bx, 002BH
push ax
push bx
pop ax
pop bx

mov ax 1000H
mov ss ax
mov sp 2H

结论

  • push、pop实质上就是一种内存传送指令,可以在寄存器和内存之间传送数据,与mov指令不同的是,push和pop指令访问的内存单元的地址不是指令中给出的,而是由SS:SP指出的。

栈是一种非常重要的机制,一定要深入理解,灵活掌握。

3.7 栈段

对于8086PC机来说,在编程时,我们可以根据需要,将一组内存单元定义为一个段。

我们可以将长度位N(N≤64k)的一组地址连续、起始地址为16的倍数的内存单元,当作栈来用,从而定义了一个栈段。

段的综述
我们可以将一段内存定义为一个段,用一个段地址指示段,用偏移地址访问段内的单元。这完全是我们自己的安排。

  • 我们可以用一个段存放数据,将它定义为"数据段";
  • 也可以用一个段存放代码,将它定义为“代码段”;
  • 还可以把一个段当作,将他定义为“栈段”。

4.2 源程序

源程序的结构
在这里插入图片描述

任务:编写运算 2 3 2^3 23

  • 定义一个段
  • 实现处理任务
  • 源程序结束
  • 段与段寄存器关联
assume cs:abc  // 实现段与段寄存器关联

abc segment // 段开始
	mov ax, 2  // 实现任务处理
	mov ax, ax
	mov ax, ax
abc ends  // 段结束

end  // 源程序结束

段结束,程序结束,程序返回

在这里插入图片描述

32位通用寄存器

32位16位
EAXAX通用寄存器,可以实现累加运算
EBXBX
ECXCX通用寄存器,但是可以用来计数
EDXDX
ESPSP堆栈指针寄存器
EBPBP堆栈基址指针寄存器
ESISI串指令源址寄存器
EDIDI串指令目的基址寄存器

MOVS指令:移动数据 内存-内存

BYTE/WORD/DWORD
MOVS BYTE PTR ES:[EDI], BYTE PTR DS:[ESI]; 简写为: MOVSB
MOVS WORD PTR ES:[EDI], WORD PTR DS:[ESI]; 简写为: MOVSW
MOVS DWORD PTR ES:[EDI], DWORD PTR DS:[ESI]; 简写为:MOVSD

例子:
MOV EDI, 12FFD8
MOV ESI, 12FFD0
MOVS DWORD PTR ES:[EDI], DWORD PTR DS:[ESI];  观察EDI的值

这次,我们对DISI有了较为明晰的认识,那就是DI表示要复制的数据串的目的地址,SI表示要复制的数据串的源地址,SI和DI都有自增自减功能。

STOS指令:将AI/AX/EAX的值存储到[EDI]指定的内存单元

STOS BYTE PTR ES:[EDI];  简写为STOSB
STOS WORD PTR ES:[EDI];  简写为STOSW
STOS DWORD PTR ES:[EDI];  简写为STOSD

MOV EAX, 12345678
MOV EDI, 12FFC4
STOS BYTE PTR ES:[EDI]
STOS WORD PTR ES:[EDI]
STOS DWOED PTR ES:[EDI]

修改标志寄存器中D位的值,然后再执行上面的指令

REP指令:按计数寄存器(ECX)指定的次数重复执行字符串指令

MOV ECX, 10H; ECX寄存器是计数寄存器,尽管他也是通用寄存器
REP MOVSD;  REP表示repeat

REP STOSD;  

堆栈相关指令

  • 堆栈本质上就是一块内存,操作系统在程序启动时已经分配好的,供程序执行时使用
  • 和数据结构的堆栈无关

push指令:
功能:
<1> 修改栈顶指针ESP寄存器
<2> 向堆栈中压入数据

push r32; 注意,存放多少位,相应的栈指针寄存器就会变化多少位,所以并不是一个固定宽度的栈指针移位
push r16
push m16
push m32
push imm8/imm16/imm32

pop指令:
功能:
<1> 将栈顶数据存储到寄存器或内存
<2> 修改栈顶指针

指令格式:

POP r32; 注意,弹出多少位,相应的栈指针寄存器就会变化多少位,所以并不是一个固定宽度的栈指针移位
POP r16
POP m16
POP m32

修改EIP的指令

jmp指令即可修改eip(instructions pointer register)的内容

MOV EIP, 寄存器/立即数/内存
简写为
JMP 寄存器/立即数/内存
注意:MOV不能对EIP进行操作

CALL指令的分解

PUSH "call"指令的下一行地址
jump "call"指令后面的立即数/寄存器/内存

call与JMP唯一的区别:在堆栈中存储了Call指令下一行的地址

RET指令分解:

POP CX
JUMP CS:[CX]; 注意,jump指令会改变EIP指令指针寄存器的值,这也是JUMP指令的实质

堆栈平衡

堆栈平衡是为了保证当前调用别的函数之前和调用函数之后,当前函数的栈指针(ESP,EBP)是不会变的。

  1. 如果要返回父程序,则当我们在堆栈中进行堆栈的操作的时候,一定要保证在RET这条指令之前,ESP指向的是我们压入栈中的地址。
  2. 如果通过堆栈传递了参数,那么在函数执行完毕后,要平衡参数传递导致的堆栈变化。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值