汇编二 - 寄存器、CS和IP、DS、Debug、栈
寄存器
- 不同CPU的寄存器是不一样的。
- 8086/win32/win64/ARM的cpu的寄存器都不一样。
- ARM是最复杂的,因为他是嵌入式的,移动端的
- CPU的典型构成
- 对程序员来说,CPU中最主要部件是寄存器,可以通过改变寄存器的内容来实现对CPU的控制
- 不同的CPU,寄存器的个数、结构是不相同的(8086是16位结构的CPU)
- 8086有14个寄存器
- 按功能可分为三类
- 8个通用寄存器
- 4个段寄存器
- 2个控制寄存器
- 都是16位的寄存器,可以存放2个字节
- 注意: 要深入理解每个寄存器中数据的含义
- 按功能可分为三类
- 寄存器的本质作用
- 存放数据
- 与运算器交互,处理数据。
- 存放地址
- 通过地址变换到内存中查找数据
- 存放数据
- 要分清楚内存跟寄存器可不是一个东西,内存是在CPU之外的存储器,寄存器是CPU内部的存储器
通用寄存器
- 通用寄存器的一级(基本)作用:
- 暂存中间运算结果(这是最基本的作用)
- 即这8个通用寄存器不管什么情况下,里面都可以放运算结果或者中间运算结果,就是用来存放数据用的
- 字节与字
- 在汇编的数据存储中,有2个比较常用的单位
- 字节:byte,1个字节由8bit组成,可以存储在8位寄存器中
- 字:word,1个字由2个字节组成,这2个字节分别称为字的高字节和低字节
- 1个字可以存在1个16位寄存器中,这个字的高字节、低字节分别存储在这个寄存器的高8位寄存器、低8位寄存器中
数据寄存器(AX,BX,CX,DX)
- 这4个数据寄存器,可以拆开使用(1个16位拆成2个8位),目的提高运用的灵活度
- 一旦拆开使用它们就是独立的寄存器了,比如,AH,AL,它们之间就没有关系了
- 但是如果AX是一个整体时,那么AH一定是高8位数据,AL是低8位数据。
- 这4个寄存器最常用的用法是用来存放中间运算结果
- 数据寄存器特有的习惯用法:(二级作用:牢记!!!)
- AX:累加器。所有I/O指令都通过AX与接口传送信息,中间运算结果也多放于AX中
- BX:基址寄存器。在间接寻址中用于存放基地址;(即BX中可能是运算数据,也可能是地址)
- CX:计数寄存器。用于在循环或串操作指令中存放计数值;
- DX:数据寄存器。在间接寻址的I/O指令中存放I/O端口地址;在32位乘除法运算时,存放高16位数。
- 数据寄存器的简单使用举例:
- 通常,CPU会先将内存中的数据存储到通用寄存器中,然后再对通用寄存器中的数据进行运算
- 假设内存中有块红色内存空间的值是3,现在想把它的值加1,并将结果存储到蓝色内存空间
- CPU首先会将红色内存空间的值放到AX寄存器中:
mov ax,红色内存空间
- 然后让AX寄存器与1相加:
add ax,1
- 最后将值赋值给内存空间:
mov 蓝色内存空间,ax
- CPU首先会将红色内存空间的值放到AX寄存器中:
地址指针寄存器(SP,BP)
- SP:堆栈指针寄存器,总是指向堆栈段的顶部。其内容为栈顶的偏移地址;
- 这个寄存器尽管作为通用寄存器,但是大多数情况下是作为专用寄存器来使用,即通常情况下就是使用它的二级作用:程序中一旦涉及到堆栈,SP一定是专用的,用来指向栈顶,即栈顶指针
- BP:基址指针寄存器,常用于在访问内存时存放内存单元的偏移地址。通常存放堆栈段的偏移地址。
- 跟SP的区别:BP可以指向堆栈的任意一个地方,而SP只能指向栈顶;而且在有堆栈的时候BP不一定作为专用寄存器指向堆栈某个位置,也可能是存放一般运算的中间运算数据
- BX与BP在应用上的区别
- 作为通用寄存器(一级作用),二者均可用于存放数据;
- 作为基址寄存器(二级作用),用BX表示所寻找的数据在数据段;用BP则表示数据在堆栈段。
- 总结:SP/BP主要用在堆栈段
变址寄存器(SI,DI)
- SI:源变址寄存器
- DI:目标变址寄存器
- 变址寄存器在指令中常用于存放数据在内存中的地址
- 二级作用:用于串操作指令中,比如:
MOVS
总结
- 这8个通用寄存器都是16位寄存器,作为16位寄存器的时候,他们里面的数据可以是运算的数据,可以是存放数据的地址(除了AX与CX之外,AX,CX只能存放数据。)
- 4个数据寄存器作为8位寄存器的时候,那这4个数据寄存器(AX,BX,CX,DX)存放的一定是数据,不可能是地址。
控制寄存器
- IP:指令指针寄存器
- 也叫程序计数器,其内容位下一条要取指令的偏移地址
- FLAGS:标志寄存器
- 保存的是运算结果的特征。
- 16位中只有9位有效,又把这9位分为2类
- 6个状态标志位(CF,SF,AF,PF,OF,ZF)
- 表示运算结果的特征。
- 3个控制标志位(IF,TF,DF)
- 表示处理器当前的工作状态
- 6个状态标志位(CF,SF,AF,PF,OF,ZF)
- 状态标志位:
- CF(Carry Flag)
- 进位标志位。加(减)法运算时,若最高位有进(借)位则CF=1
- PF(Parity Flag)
- 奇偶标志位。运算结果的低8位中“1”的个数为偶数时PF=l
- AF(Auxiliary Carry Flag)
- 辅助进位标志位。加(减)操作中,若Bit3向Bit4有进位(借位),AF=1
- ZF(Zero Flag)
- 零标志位。当运算结果为零时ZF=1
- SF(Sign Flag)
- 符号标志位。当运算结果的最高位为1时,SF=l
- OF(Overflow Flag)
- 溢出标志位。当算术运算的结果超出了有符号数的可表达范围时,OF=l
- CF(Carry Flag)
- 控制标志位
- TF(Trap Flag)
- 陷井标志位,也叫跟踪标志位。TF=1时,使CPU处于单步执行指令的工作方式。
- IF(Interrupt Enable Flag)
- 中断允许标志位。IF=1使CPU可以响应可屏蔽中断请求。
- DF(Direction Flag)
- 方向标志位。在数据串操作时确定操作的方向。
- TF(Trap Flag)
段寄存器
- 作用:
- 内存被分为很多逻辑段,但是类型只有四种(数据、代码、附加、堆栈)
- 用于存放内存中相应逻辑段的段基地址(就是该段的首地址)
- 就是为了寻址的:段基地址*16 + 偏移地址 = 物理地址
- 这些段寄存器初始化在程序启动内存分配后
- 8088/8086内存中的逻辑段类型(逻辑段有很多,但是类型只有四种)
- 代码段:存放指令代码
- 数据段: 存放操作的数据
- 附加段:存放操作的数据
- 堆栈段: 存放暂时不用但需保存的数据
- 内存中这4中类型的段,就要对应每个段的段地址,那么段地址放在哪呢? –> 段寄存器
- 代码段寄存器(CS):存放代码段的段基地址。
- 数据段寄存器(DS):存放数据段的段基地址。
- 附加段寄存器(ES):存放数据段的段基地址。
- 堆栈段寄存器(SS):存放堆栈段的段基地址
- 问题:
- 既然上面说,内存可以被分为n个段,但是类型只有4种,但是段寄存器智能存放4个类型的段地址,那怎么办呢?(比如内存中分别有4个代码、数据、附加、堆栈段,那么段基地址就有16个,那么段寄存器只有4个,放不下)
- 解决办法:
- 由于段寄存器只有4个,而且是每一种一个,那么就决定了:一个程序模块里最多只能有4个逻辑段,而且每个逻辑段都是唯一的!!!
- 通俗点来说,内存中分了n个代码段,n个堆栈段,n个数据段,n个附加段,我写了个app程序,但是这个app程序,内存只会给他分配1个代码段,1个堆栈段,1个数据段,1个附加段,
- 而且每个段的首地址(段基地址)放在了CPU的事个段寄存器中
- 因此段寄存器的值表明相应逻辑段在内存中的位置
- 总结:
- 8086在访问内存时要由相关部件提供内存单元的段地址和偏移地址,送入地址加法器合成物理地址
- 是什么部件提供段地址?段地址在8086的段寄存器中存放
- 8086有4个段寄存器:CS、DS、SS、ES,当CPU需要访问内存时由这4个段寄存器提供内存单元的段地址
- CS (Code Segment):代码段寄存器
- DS (Data Segment):数据段寄存器
- SS (Stack Segment):堆栈段寄存器
- ES (Extra Segment):附加段寄存器
CS和IP
- CS为代码段寄存器,IP为指令指针寄存器,它们指示了CPU当前要读取指令的地址
- 任意时刻,8086CPU都会将CS:IP指向的指令作为下一条需要取出执行的指令
- 指令执行的过程
- CPU根据代码段寄存器CS的值、IP寄存器的值,通过地址加法器合成20位的内存地址,传送给输入输出控制电路
- 输入输出控制电路通过地址地址总线将20位地址传输到内存,在内存的代码段找到对应的内存
- 寻址到内存中的数据通过数据总线传输给CPU的输入输出控制电路
- 输入输出控制电路,将数据传输给指令缓冲器
- 指令缓冲器将数据发送给执行控制器,执行控制指令
-
IP寄存器自动加上一条指令的字节个数,然后执行下一条指令
- 通过上面的过程展示,8086CPU的工作过程可以简要描述如下
- 从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器
- 此时CS为2000H,IP为0000H,合成地址为20000H
- 取出内存单元20000H开始的3个字节,为0123B8
- 0123B8就是指令
mov ax,0123H
这条指令对应的16进制码 - 将0123B8通过数据总线传送给指令缓冲器。
- IP = IP+所读取指令的长度,从而指向下一条指令
- 此时的IP为0003H
- 执行指令。跳转到步骤1,重复整个过程
- 执行控制器执行指令
0123B8
- 将0123H存入到AX寄存器。
- 执行控制器执行指令
- 从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器
- 在8086CPU加电启动或复位后(即CPU刚开始工作时)CS和IP被设置为CS = FFFFH,IP = 0000H,即在8086PC机刚刚启动时,CPU从内存FFFF0H单元中读取指令执行,FFFF0H单元中的指令是8086PC机开机后执行的第一条指令。
- 注意:
- 偏移地址IP只为CS服务,即代码段服务
- 而其他段(数据段、堆栈段、附加段)的偏移地址不是由IP提供
- 指令和数据
- 在内存或者磁盘上,指令和数据没有任何区别,都是二进制信息
- CPU在工作的时候把有的信息看做指令,有的信息看做数据,为同样的信息赋予了不同的意义
- CPU根据什么将内存中的信息看做指令?
- CPU将CS:IP指向的内存单元的内容看做指令
- 取决于用哪种段寄存器来取值
jmp指令
- CPU从何处执行指令是由CS、IP中的内容决定的,我们可以通过改变CS、IP的内容来控制CPU执行目标指令
- 8086提供了一个mov指令(传送指令),可以用来修改大部分寄存器的值,比如:
mov ax,10、mov bx,20、mov cx,30、mov dx,40
- 但是,mov指令不能用于设置CS、IP的值,8086没有提供这样的功能
- 8086提供了另外的指令来修改CS、IP的值,这些指令统称为转移指令,最简单的是jmp指令
- 若想同时修改CS/IP的内容,可用形如:“
jmp 段地址:偏移地址
” 的指令完成。如:jmp 2AE3:3
,执行后:CS=2AE3H,IP= 0003H
,CPU将从2AE33H处读取指令。
- ”
jmp 段地址:偏移地址
“ 指令的功能为: 用指令给出的段地址修改CS,偏移地址修改IP -
若想修改IP的内容,可用形如“
jmp 某一合法寄存器
”的指令完成,如:jmp ax, 执行指令前:ax = 1000H,CS = 2000H,IP= 0003H 执行指令后: ax = 1000H ,CS = 2000H,IP = 1000H
jmp 某一合法寄存器
指令的功能为: 用寄存器中的值修改IPjmp ax
,在含义上好似:mov IP, AX
.
- 另外,也可以“
jmp 直接值
”来改变IP的值,比如“jmp 0100H
”
DS寄存器
- CPU要读写一个内存单元时,必须要先给出这个内存单元的地址,在8086中,内存地址由段地址和偏移地址组成,格式:
段地址:[偏移地址]
-
8086中有一个DS段寄存器,通常用来存放要访问数据的段地址
;初始化数据段寄存器DS为1000h mov bx,1000h mov ds,bx ;将:DS:[0]内存数据传递给al寄存器 mov al,[0]
- 上面3条指令的作用将10000H(1000:0)中的内存数据赋值到al寄存器中
- mov al,[address]的意思将DS:address中的内存数据赋值到al寄存器中
- 由于al是8位寄存器,所以是将一个字节的数据赋值给al寄存器
- 注意: 8086不支持将数据直接送入段寄存器中,mov ds,1000H是错误的
- 8086规定,不写段地址,默认为数据段
-
例2:
;访问内存的数据: 段地址:[偏移地址] mov ax,1123h ;定义数据段地址 ;mov ds, 1000h 这句话是错误的,因为不能直接给段寄存器传值(mov) ;mov 常用语内存、通用寄存器 mov bx,1000h mov ds,bx ;mov ds:[0h],ax 这句话可以被下面的替代,因为8086规定,不写段地址,默认为数据段 mov [0h],ax
大小端
- 就是内存存储数据的一种模式
- 大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中(高低\低高) (Big Endian)
- 小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中(高高\低低) (Little Endian)
指明指令要处理的数据长度
-
比如下面两条指令:
mov [0h],66h ;这句话是往内存中传递一个字节的数据 mov [0h],0066h ;这句话是往内存中传递2个字节的数据这两者是不一样的。
- 8086指令能处理2种尺寸的数据:byte、word
-
思考:
mov [0], 20H
指令是否正确?mov byte ptr [0], 20H 将20H放入0位置内存的字节单元,占用1个字节 mov word ptr [0], 20H 将20H放入0位置内存的字单元,占用2个字节
-
很多指令都可以通过“byte ptr”或者“word ptr”来指明所需要操作内存的数据长度
inc byte ptr [0] add word ptr [0], 2
- 有些指令有默认的操作数据长度,比如
push [0]、pop [0]
的操作数据长度只能是2个字节
栈
- 栈:是一种具有特殊的访问方式的存储空间(后进先出, Last In Out Firt,LIFO)
- 8086会将CS作为代码段的段地址,将
CS:IP
指向的指令作为下一条需要取出执行的指令 - 8086会将DS作为数据段的段地址,
mov ax,[address]
就是取出DS:address
的内存数据放到ax寄存器中 - 8086会将SS作为栈段的段地址,任意时刻,
SS:SP
指向栈顶元素 - 8086提供了PUSH(入栈)和POP (出栈)指令来操作栈段的数据
- 比如
push ax
是将ax的数据入栈,pop ax
是将栈顶的数据送入ax
- 比如
-
push ax
- 从这张图可以看出,栈里面压入数据,sp即栈顶是从高地址相低地址下减的。
- push ax的执行,有以下两步骤完成:
- sp = sp -2,ss:sp指向当前的栈顶前面的单元,以当前栈顶前面的单元为新的栈顶;
- 将ax中的内容送入ss:sp指向的内存单元处,ss:sp此时指向新栈顶。
-
pop ax
- pop ax的执行过程和push ax恰好相反
- 将ss:sp指向的内存单元处的数据送入ax中;
- sp = sp +2, ss:sp指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。
- pop ax的执行过程和push ax恰好相反
- 思考:
-
如果将10000H~1000FH这段空间当做栈,初始状态栈是空的,此时,ss = 10000H,sp=?
- 空栈,ss:sp指向栈空间最高地址单元的下一个单元
-
- 栈顶超界
- 当栈满的时候,在使用push指令入栈,或栈空的时候在使用pop指令出栈,都将发送栈顶超界问题
- 栈顶超界是危险的,因为我们既然将一段空间安排为栈,那么在栈空间之外的空间里很可能存放了具有其他用途的数据、代码等,这些数据、代码可能是我们自己程序中的,也可能是别的程序中的(毕竟一个计算机系统中并不是只有我们自己的程序在运行)。但是由于我们在入栈、出栈是不小心,而将这些数据、代码意外的改写,将会引发一连串的错误。
- 当然我们希望cpu可以帮我们解决这个问题,比如说在CPU中有记录栈顶上限和栈底的寄存器,我们可以通过填写这些寄存器来指定栈空间的范围,然后,CPU在执行push指令的时候靠检测栈顶上限寄存器、在执行pop指令的时候靠检测栈底寄存器保证不会超界。
- 但是8086CPU并没有这样的寄存器,因此,我们在编程的时候要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致超界问题;执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界。
- 栈的总结
- 我们可以用一个段存放数据,将它定义为数据段
- 我们可以用一个段存放代码,将它定义为代码段
- 我们可以用一个段当做栈,将它定义为栈段
- 对于数据段,将他的段地址放在DS中,用mov、add、sub等访问内存单元的指令时,CPU就将我们定义的数据段中的内容当做数据来访问。
- 对于代码段,将它的段地址放在CS中,将段中第一条指令的偏移地址放在IP中,这样CPU就将执行我们定义的代码段中的指令。
- 对于栈段,将它的段地址放在SS中,将栈顶单元的偏移地址放在SP中,这样CPU在需要进行栈操作的时候,比如执行push、pop指令等,就将我们定义的栈段当做栈空间来用。