汇编二 - 寄存器、CS和IP、DS、Debug、栈

寄存器

  1. 不同CPU的寄存器是不一样的。
    1. 8086/win32/win64/ARM的cpu的寄存器都不一样。
    2. ARM是最复杂的,因为他是嵌入式的,移动端的
  2. CPU的典型构成 图1
    1. 对程序员来说,CPU中最主要部件是寄存器,可以通过改变寄存器的内容来实现对CPU的控制
    2. 不同的CPU,寄存器的个数、结构是不相同的(8086是16位结构的CPU)
    3. 8086有14个寄存器
      1. 按功能可分为三类
        1. 8个通用寄存器
        2. 4个段寄存器
        3. 2个控制寄存器
      2. 都是16位的寄存器,可以存放2个字节
      3. 注意: 要深入理解每个寄存器中数据的含义 图1
  3. 寄存器的本质作用
    1. 存放数据
      1. 与运算器交互,处理数据。
    2. 存放地址
      1. 通过地址变换到内存中查找数据
  4. 要分清楚内存跟寄存器可不是一个东西,内存是在CPU之外的存储器,寄存器是CPU内部的存储器

通用寄存器

  1. 通用寄存器的一级(基本)作用
    1. 暂存中间运算结果(这是最基本的作用)
    2. 即这8个通用寄存器不管什么情况下,里面都可以放运算结果或者中间运算结果,就是用来存放数据用的
  2. 字节与字
    1. 在汇编的数据存储中,有2个比较常用的单位
    2. 字节:byte,1个字节由8bit组成,可以存储在8位寄存器中
    3. 字:word,1个字由2个字节组成,这2个字节分别称为字的高字节和低字节
    4. 1个字可以存在1个16位寄存器中,这个字的高字节、低字节分别存储在这个寄存器的高8位寄存器、低8位寄存器中

数据寄存器(AX,BX,CX,DX)

  1. 这4个数据寄存器,可以拆开使用(1个16位拆成2个8位),目的提高运用的灵活度
    1. 一旦拆开使用它们就是独立的寄存器了,比如,AH,AL,它们之间就没有关系了
    2. 但是如果AX是一个整体时,那么AH一定是高8位数据,AL是低8位数据。
  2. 这4个寄存器最常用的用法是用来存放中间运算结果
  3. 数据寄存器特有的习惯用法:(二级作用:牢记!!!
    1. AX:累加器。所有I/O指令都通过AX与接口传送信息,中间运算结果也多放于AX中
    2. BX:基址寄存器。在间接寻址中用于存放基地址;(即BX中可能是运算数据,也可能是地址)
    3. CX:计数寄存器。用于在循环或串操作指令中存放计数值;
    4. DX:数据寄存器。在间接寻址的I/O指令中存放I/O端口地址;在32位乘除法运算时,存放高16位数。
  4. 数据寄存器的简单使用举例:
    1. 通常,CPU会先将内存中的数据存储到通用寄存器中,然后再对通用寄存器中的数据进行运算
    2. 假设内存中有块红色内存空间的值是3,现在想把它的值加1,并将结果存储到蓝色内存空间
      1. CPU首先会将红色内存空间的值放到AX寄存器中:mov ax,红色内存空间
      2. 然后让AX寄存器与1相加:add ax,1
      3. 最后将值赋值给内存空间:mov 蓝色内存空间,ax

地址指针寄存器(SP,BP)

  1. SP:堆栈指针寄存器,总是指向堆栈段的顶部。其内容为栈顶的偏移地址;
    1. 这个寄存器尽管作为通用寄存器,但是大多数情况下是作为专用寄存器来使用,即通常情况下就是使用它的二级作用:程序中一旦涉及到堆栈,SP一定是专用的,用来指向栈顶,即栈顶指针
  2. BP:基址指针寄存器,常用于在访问内存时存放内存单元的偏移地址。通常存放堆栈段的偏移地址
    1. 跟SP的区别:BP可以指向堆栈的任意一个地方,而SP只能指向栈顶;而且在有堆栈的时候BP不一定作为专用寄存器指向堆栈某个位置,也可能是存放一般运算的中间运算数据
  3. BX与BP在应用上的区别
    1. 作为通用寄存器(一级作用),二者均可用于存放数据;
    2. 作为基址寄存器(二级作用),用BX表示所寻找的数据在数据段;用BP则表示数据在堆栈段
  4. 总结:SP/BP主要用在堆栈段

变址寄存器(SI,DI)

  1. SI:源变址寄存器
  2. DI:目标变址寄存器
  3. 变址寄存器在指令中常用于存放数据在内存中的地址
  4. 二级作用:用于串操作指令中,比如: MOVS

总结

  1. 这8个通用寄存器都是16位寄存器,作为16位寄存器的时候,他们里面的数据可以是运算的数据,可以是存放数据的地址(除了AX与CX之外,AX,CX只能存放数据。)
  2. 4个数据寄存器作为8位寄存器的时候,那这4个数据寄存器(AX,BX,CX,DX)存放的一定是数据,不可能是地址。

控制寄存器

  1. IP:指令指针寄存器
    1. 也叫程序计数器,其内容位下一条要取指令的偏移地址
  2. FLAGS:标志寄存器
    1. 保存的是运算结果的特征。
    2. 16位中只有9位有效,又把这9位分为2类
      1. 6个状态标志位(CF,SF,AF,PF,OF,ZF)
        1. 表示运算结果的特征。
      2. 3个控制标志位(IF,TF,DF)
        1. 表示处理器当前的工作状态
    3. 状态标志位:
      1. CF(Carry Flag)
        1. 进位标志位。加(减)法运算时,若最高位有进(借)位则CF=1
      2. PF(Parity Flag)
        1. 奇偶标志位。运算结果的低8位中“1”的个数为偶数时PF=l
      3. AF(Auxiliary Carry Flag)
        1. 辅助进位标志位。加(减)操作中,若Bit3向Bit4有进位(借位),AF=1
      4. ZF(Zero Flag)
        1. 零标志位。当运算结果为零时ZF=1
      5. SF(Sign Flag)
        1. 符号标志位。当运算结果的最高位为1时,SF=l
      6. OF(Overflow Flag)
        1. 溢出标志位。当算术运算的结果超出了有符号数的可表达范围时,OF=l  
    4. 控制标志位
      1. TF(Trap Flag)
        1. 陷井标志位,也叫跟踪标志位。TF=1时,使CPU处于单步执行指令的工作方式。
      2. IF(Interrupt Enable Flag)
        1. 中断允许标志位。IF=1使CPU可以响应可屏蔽中断请求。
      3. DF(Direction Flag)
        1. 方向标志位。在数据串操作时确定操作的方向。

段寄存器

  1. 作用:
    1. 内存被分为很多逻辑段,但是类型只有四种(数据、代码、附加、堆栈)
    2. 用于存放内存中相应逻辑段的段基地址(就是该段的首地址)
    3. 就是为了寻址的:段基地址*16 + 偏移地址 = 物理地址
    4. 这些段寄存器初始化在程序启动内存分配后
  2. 8088/8086内存中的逻辑段类型(逻辑段有很多,但是类型只有四种)
    1. 代码段:存放指令代码
    2. 数据段: 存放操作的数据
    3. 附加段:存放操作的数据
    4. 堆栈段: 存放暂时不用但需保存的数据
  3. 内存中这4中类型的段,就要对应每个段的段地址,那么段地址放在哪呢? –> 段寄存器
    1. 代码段寄存器(CS):存放代码段的段基地址。
    2. 数据段寄存器(DS):存放数据段的段基地址。
    3. 附加段寄存器(ES):存放数据段的段基地址。
    4. 堆栈段寄存器(SS):存放堆栈段的段基地址
  4. 问题:
    1. 既然上面说,内存可以被分为n个段,但是类型只有4种,但是段寄存器智能存放4个类型的段地址,那怎么办呢?(比如内存中分别有4个代码、数据、附加、堆栈段,那么段基地址就有16个,那么段寄存器只有4个,放不下)
    2. 解决办法:
      1. 由于段寄存器只有4个,而且是每一种一个,那么就决定了:一个程序模块里最多只能有4个逻辑段,而且每个逻辑段都是唯一的!!!
      2. 通俗点来说,内存中分了n个代码段,n个堆栈段,n个数据段,n个附加段,我写了个app程序,但是这个app程序,内存只会给他分配1个代码段,1个堆栈段,1个数据段,1个附加段,
      3. 而且每个段的首地址(段基地址)放在了CPU的事个段寄存器中
      4. 因此段寄存器的值表明相应逻辑段在内存中的位置
  5. 总结:
    1. 8086在访问内存时要由相关部件提供内存单元的段地址和偏移地址,送入地址加法器合成物理地址
    2. 是什么部件提供段地址?段地址在8086的段寄存器中存放
    3. 8086有4个段寄存器:CS、DS、SS、ES,当CPU需要访问内存时由这4个段寄存器提供内存单元的段地址
      1. CS (Code Segment):代码段寄存器
      2. DS (Data Segment):数据段寄存器
      3. SS (Stack Segment):堆栈段寄存器
      4. ES (Extra Segment):附加段寄存器

CS和IP

  1. CS为代码段寄存器,IP为指令指针寄存器,它们指示了CPU当前要读取指令的地址
  2. 任意时刻,8086CPU都会将CS:IP指向的指令作为下一条需要取出执行的指令
  3. 指令执行的过程
    1. CPU根据代码段寄存器CS的值、IP寄存器的值,通过地址加法器合成20位的内存地址,传送给输入输出控制电路
    2. 输入输出控制电路通过地址地址总线将20位地址传输到内存,在内存的代码段找到对应的内存
    3. 寻址到内存中的数据通过数据总线传输给CPU的输入输出控制电路
    4. 输入输出控制电路,将数据传输给指令缓冲器
    5. 指令缓冲器将数据发送给执行控制器,执行控制指令
    6. IP寄存器自动加上一条指令的字节个数,然后执行下一条指令 图1

    7. 通过上面的过程展示,8086CPU的工作过程可以简要描述如下
      1. 从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器
        1. 此时CS为2000H,IP为0000H,合成地址为20000H
        2. 取出内存单元20000H开始的3个字节,为0123B8
        3. 0123B8就是指令mov ax,0123H这条指令对应的16进制码
        4. 将0123B8通过数据总线传送给指令缓冲器。
      2. IP = IP+所读取指令的长度,从而指向下一条指令
        1. 此时的IP为0003H
      3. 执行指令。跳转到步骤1,重复整个过程
        1. 执行控制器执行指令0123B8
        2. 将0123H存入到AX寄存器。
    8. 在8086CPU加电启动或复位后(即CPU刚开始工作时)CS和IP被设置为CS = FFFFH,IP = 0000H,即在8086PC机刚刚启动时,CPU从内存FFFF0H单元中读取指令执行,FFFF0H单元中的指令是8086PC机开机后执行的第一条指令。
    9. 注意:
      1. 偏移地址IP只为CS服务,即代码段服务
      2. 而其他段(数据段、堆栈段、附加段)的偏移地址不是由IP提供
  4. 指令和数据
    1. 在内存或者磁盘上,指令和数据没有任何区别,都是二进制信息
    2. CPU在工作的时候把有的信息看做指令,有的信息看做数据,为同样的信息赋予了不同的意义
    3. CPU根据什么将内存中的信息看做指令?
      1. CPU将CS:IP指向的内存单元的内容看做指令
      2. 取决于用哪种段寄存器来取值

jmp指令

  1. CPU从何处执行指令是由CS、IP中的内容决定的,我们可以通过改变CS、IP的内容来控制CPU执行目标指令
  2. 8086提供了一个mov指令(传送指令),可以用来修改大部分寄存器的值,比如:
    1. mov ax,10、mov bx,20、mov cx,30、mov dx,40
  3. 但是,mov指令不能用于设置CS、IP的值,8086没有提供这样的功能
  4. 8086提供了另外的指令来修改CS、IP的值,这些指令统称为转移指令,最简单的是jmp指令
  5. 若想同时修改CS/IP的内容,可用形如:“jmp 段地址:偏移地址” 的指令完成。如:
    1. jmp 2AE3:3,执行后:CS=2AE3H,IP= 0003H,CPU将从2AE33H处读取指令。
  6. jmp 段地址:偏移地址“ 指令的功能为: 用指令给出的段地址修改CS,偏移地址修改IP
  7. 若想修改IP的内容,可用形如“jmp 某一合法寄存器”的指令完成,如:

     jmp ax, 执行指令前:ax = 1000H,CS = 2000H,IP= 0003H
             执行指令后: ax = 1000H ,CS = 2000H,IP = 1000H
    
    1. jmp 某一合法寄存器指令的功能为: 用寄存器中的值修改IP
    2. jmp ax ,在含义上好似: mov IP, AX.
  8. 另外,也可以“jmp 直接值”来改变IP的值,比如“jmp 0100H

DS寄存器

  1. CPU要读写一个内存单元时,必须要先给出这个内存单元的地址,在8086中,内存地址由段地址和偏移地址组成,格式:段地址:[偏移地址]
  2. 8086中有一个DS段寄存器,通常用来存放要访问数据的段地址

     ;初始化数据段寄存器DS为1000h
     mov bx,1000h
     mov ds,bx
     ;将:DS:[0]内存数据传递给al寄存器
     mov al,[0]
    
    1. 上面3条指令的作用将10000H(1000:0)中的内存数据赋值到al寄存器中
    2. mov al,[address]的意思将DS:address中的内存数据赋值到al寄存器中
    3. 由于al是8位寄存器,所以是将一个字节的数据赋值给al寄存器
    4. 注意: 8086不支持将数据直接送入段寄存器中,mov ds,1000H是错误的
    5. 8086规定,不写段地址,默认为数据段
    6. 例2:

       ;访问内存的数据: 段地址:[偏移地址]  
       mov ax,1123h   
       ;定义数据段地址
       ;mov ds, 1000h  这句话是错误的,因为不能直接给段寄存器传值(mov) 
       ;mov 常用语内存、通用寄存器
       mov bx,1000h
       mov ds,bx  
       ;mov ds:[0h],ax  这句话可以被下面的替代,因为8086规定,不写段地址,默认为数据段 
       mov [0h],ax
      

大小端

  1. 就是内存存储数据的一种模式
  2. 大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中(高低\低高) (Big Endian)
  3. 小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中(高高\低低) (Little Endian)

指明指令要处理的数据长度

  1. 比如下面两条指令:

     mov [0h],66h   ;这句话是往内存中传递一个字节的数据
     mov [0h],0066h ;这句话是往内存中传递2个字节的数据这两者是不一样的。
    
  2. 8086指令能处理2种尺寸的数据:byte、word
  3. 思考:mov [0], 20H指令是否正确?

     mov byte ptr [0], 20H  将20H放入0位置内存的字节单元,占用1个字节
     mov word ptr [0], 20H  将20H放入0位置内存的字单元,占用2个字节
    
  4. 很多指令都可以通过“byte ptr”或者“word ptr”来指明所需要操作内存的数据长度

     inc byte ptr [0]
     add word ptr [0], 2
    
  5. 有些指令有默认的操作数据长度,比如push [0]、pop [0]的操作数据长度只能是2个字节

  1. 栈:是一种具有特殊的访问方式的存储空间(后进先出, Last In Out Firt,LIFO)
  2. 8086会将CS作为代码段的段地址,将CS:IP指向的指令作为下一条需要取出执行的指令
  3. 8086会将DS作为数据段的段地址,mov ax,[address]就是取出DS:address的内存数据放到ax寄存器中
  4. 8086会将SS作为栈段的段地址,任意时刻,SS:SP指向栈顶元素
  5. 8086提供了PUSH(入栈)和POP (出栈)指令来操作栈段的数据
    1. 比如push ax是将ax的数据入栈,pop ax是将栈顶的数据送入ax
  6. push ax

    图1

    1. 从这张图可以看出,栈里面压入数据,sp即栈顶是从高地址相低地址下减的。
    2. push ax的执行,有以下两步骤完成:
      1. sp = sp -2,ss:sp指向当前的栈顶前面的单元,以当前栈顶前面的单元为新的栈顶;
      2. 将ax中的内容送入ss:sp指向的内存单元处,ss:sp此时指向新栈顶。
  7. pop ax

    图1

    1. pop ax的执行过程和push ax恰好相反
      1. 将ss:sp指向的内存单元处的数据送入ax中;
      2. sp = sp +2, ss:sp指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。
  8. 思考:
    1. 如果将10000H~1000FH这段空间当做栈,初始状态栈是空的,此时,ss = 10000H,sp=?

      图1

      1. 空栈,ss:sp指向栈空间最高地址单元的下一个单元
  9. 栈顶超界
    1. 当栈满的时候,在使用push指令入栈,或栈空的时候在使用pop指令出栈,都将发送栈顶超界问题
    2. 栈顶超界是危险的,因为我们既然将一段空间安排为栈,那么在栈空间之外的空间里很可能存放了具有其他用途的数据、代码等,这些数据、代码可能是我们自己程序中的,也可能是别的程序中的(毕竟一个计算机系统中并不是只有我们自己的程序在运行)。但是由于我们在入栈、出栈是不小心,而将这些数据、代码意外的改写,将会引发一连串的错误。
    3. 当然我们希望cpu可以帮我们解决这个问题,比如说在CPU中有记录栈顶上限和栈底的寄存器,我们可以通过填写这些寄存器来指定栈空间的范围,然后,CPU在执行push指令的时候靠检测栈顶上限寄存器、在执行pop指令的时候靠检测栈底寄存器保证不会超界。
    4. 但是8086CPU并没有这样的寄存器,因此,我们在编程的时候要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致超界问题;执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界。
  10. 栈的总结
    1. 我们可以用一个段存放数据,将它定义为数据段
    2. 我们可以用一个段存放代码,将它定义为代码段
    3. 我们可以用一个段当做栈,将它定义为栈段
    4. 对于数据段,将他的段地址放在DS中,用mov、add、sub等访问内存单元的指令时,CPU就将我们定义的数据段中的内容当做数据来访问。
    5. 对于代码段,将它的段地址放在CS中,将段中第一条指令的偏移地址放在IP中,这样CPU就将执行我们定义的代码段中的指令。
    6. 对于栈段,将它的段地址放在SS中,将栈顶单元的偏移地址放在SP中,这样CPU在需要进行栈操作的时候,比如执行push、pop指令等,就将我们定义的栈段当做栈空间来用。
Table of Contents