Power by GeekHades

汇编-讨论寄存器以及对程序的思考

0x1 前言

在编写汇编语言的过程中,寄存器(Register)的地位就像是我们的身体内的各个不同的器官。相互协调。本文以MASM(Microsoft Macro Assembler)的设计规范为探讨范围,对寄存器进行更加深入的探讨,同时结合高级语言进行理解。通过分析原理来解释为什么有些指令我们看上去貌似正确,但其实是错误的。例如:

; 第一种
mov ax, [cx]            
mov ax, [ax]
mov ax, [bx]
; 第二种
mov ax, [bx+bp]
mov ax, [si+di]
; 第三种
add ds:[bx], [bp]

0x2 寄存器的种类

寄存器总体可以被划分为 大类: - 通用寄存器(General registers) - 控制寄存器(Control registers) - 段寄存器(Segment registers)

在MASM的设计中 通用寄存器 又被细分为以下 类: - 数据寄存器(Data registers) - 指针寄存器(Pointer registers) - 索引寄存器(Index registers)

如下表:

通用寄存器 控制寄存器(标志位) 段寄存器
数据寄存器 指针寄存器 段寄存器
AX (32-bit as EAX) IP (32-bit as EIP) SI (32-bit as ESI) OF、DF、IF等等 CS (Code Segment)
BX (32-bit as EBX) SP (32-bit as ESP) DI (32-bit as EDI) DS (Data Segment)
CX (32-bit as ECX) BP (32-bit as EBP) SS (Stack Segment)
DX (32-bit as EDX) 额外还有ES, FS, GS等

数据寄存器

Data registers

  • AX 是一个累加寄存器,它由低八位 AL 和 高八位 AH共同组成。AX几乎是一种万能型的寄存器 mov操作很多时候都会与AX进行配合。

  • BX是一个基址寄存器(Base register),一般是用来进行基址寻址(寄存器间接寻址),如果不显式的说明段地址,它将默认的设置DS中的内容为段地址

; 假设DS = 1000H ES = 2000H
mov bx, 0H
mov ax, [bx]                ;  ds:bx  1000:0 单元内容传输给 ax
mov ax, es:[bx]             ; 显式的说明段地址 es:bx  2000:0 单元内容传输给 ax
  • CX是计数寄存器,一般存放着LOOP循环指令的剩余执行次数。

这里我们就可以尝试去解释文章开头所说的第一种错误类型,假设我们使用mov ax, [cx]这条指令,我们原意是看ds:cx单元中的内容,但是一旦编译就会得到以下错误提示: error A2048: Must be index or base register 进行寄存器间接寻址必须是索引寄存器或者是基址寄存器。所以在使用这种寻址方式时,除了这两类寄存器可以正确执行之外,其他的都是错误的。

  • DX数据寄存器,普通情况下它的使用限制与AX是一样的,但是在进行乘除法时,就会使用DX来保存超长数据(同时也是与AX进行配合)

指针寄存器

Pointer register

  • IP (Instruction Pointer)指令指针寄存器(同样也是16位的,图中没有画出来)。这个寄存器是非常重要的,它保存着我们当前运行的程序的偏移地址。 run1
OB28:0000:      mov ax, 1000H       ; CS = 0B28, IP = 0000
0B28:0003:      mov bx, 1002H       ; CS = 0B28, IP = 0003

所以请谨记一点,不是非常有必要的条件下,不要修改IP的内容。因为这不仅仅只是导致你的程序运行错误,还是导致其他程序运行错误。

  • SP (Stack Pointer)栈指针寄存器,这个寄存器同样也是保存偏移地址(偏移量),只不过它是保存的栈的偏移量
assume cs:code, ss:stack

stack segment
    db 8 DUP(0)
stack ends

code segment
start:      mov ax, stack
            mov ss, ax          ; 定义 栈的段地址
            mov sp, 10H         ; 将 SP 指定到栈顶位置

            PUSH ax             ; SP = SP - 2
            PUSH ax             ; SP = SP - 2

            POP ax              ; SP = SP + 2
            POP ax              ; SP = SP + 2

            mov ax, 4c00H
            int 21H
code ends

end start

至于栈的操作机制我们这里不详细论述。

  • BP(Base Pointer)基址指针寄存器,一般用于给子程序传递参数,BPBX的区别需要注意。不特别声明段地址的情况下,默认使用SS的内容作为段地址,其实这很好理解,在类似C\C++Java这种高级语言中,函数间的参数传递使用的就是栈传递的,所以BP会默认使用SS的内容作为段地址。
; 假设 DS = 1000H, SS = 1010H
mov ax, [bx]                ; 即 ds:bx 内容传输给ax
mov ax, [bp]                ; 即 ss:bp 内容传输给ax

到这里我们又可以尝试去解释第二种错误mov ax, [bx+bp] mov ax, [si+di](即使还没有探讨到SI,DI),也许我们的原意是想将(ds:(bx+bp)) bx+bp相加获得最终偏移地址的内容传输给AX,但是单我们进行编译的时候会得到以下错误 error A2046: Multiple base registers 很多资料都没有详细的说过为什么过多的基址寄存器或者变址寄存器配合寻址会出现错误,可能这涉及到复杂的CPU的总线设计问题,我个人猜测BXBP,SIDI这两类寄存器同时使用会干扰到地址加法器的正确计算。

索引寄存器

index registers - SI (Source Index)源地址寄存器,一般与DI无区别使用,用来做变址寻址 - DI (Destination Index)目的地址寄存器。

我个人觉得SI,DI更加类似高级语言中的二维数组的下标定位,下面我以数组内容求和作为例子说明SI,DI的应用,与高级语言做对比 在C中

main(){
    int sum = 0;
    int array[10][10];
    /* 这里假设array已经填充完数据 */
    for(int i = 0; i < 10; i++)
        for(int j = 0; j < 10; j++)
            sum += array[i][j]
}

而在汇编中是这样实现的,我这里还是选择将全部代码贴出来。当然这两个程序还是不等价的,因为高级语言封装了基本数据类型,所以他们的字节数与汇编的是有出入的。这并无大碍。

assume cs:code, ds:data, ss:stack

data segment
    db 400 DUP(0)           ; 对应array[10][10] 4*100个字节
data ends

stack segment
    db 8 DUP(0)             ; 定义栈空间
stack ends

code segment
start:      mov ax, data    ; 将data段的段地址传送给ax
            mov ds, ax      ; 初始化ds
            ; 初始化栈
            mov ax, stack   
            mov ss, ax
            mov sp, 10H
            ; 设置地址偏移
            mov bx, 0
            mov ax, 0       ; 等价于 sum = 0 

            mov cx, 10      ; 设置循环次数

i:          PUSH cx         ; 将外循环的次数压入栈中
            mov si, 0       ; 设置第二维的索引
            mov cx, 10      ; 设置内循环次数

j:          add ax, [bx+si] ; 等价于 sum += array[i][j]
            inc si          ; 等价于 j++
            LOOP j

            POP cx          ; 将外循环次数从栈中弹出来
            inc bx          ; 等价于 i++
            LOOP i          ; 继续执行第一维循环

            mov ax, 4c00H
            int 21H
code ends

end start

0x3 第三种错误

我们常用的寄存器基本已经探讨完了,现在我们该来探讨以下最后一个错误的原因。现在请看一下在高级语言中的一个例子

int main(){
    int a = 1, b = 2;
    int *ap = &a, *bp = &b;
    // 这里我们将 a 内存中的内容 与 b 内存中的内容相加的结果 写入到 a 的内存中
    // 这是在任何高级语言都能轻易做到的
    *ap = (*ap) + (*bp);
    return 0;

接下来看一下我们的错误例子add ds:[bx], [bp]。我们的原意可能是想将ds:bx内存单元中的内容与ss:[bp]内存单元中的内容相加然后结果保存到ds:[bx] 等价于C++中的*ap = (*ap) + (*bp);。这是所有写惯了高级语言的人写汇编语言都会犯的通病,只能说高级语言帮程序员做了太多的事情,导致我们对计算机原本的样子并不算了解。 当编译add ds:[bx], [bp]时会得到以下错误: error A2052: Improper operand type 操作数类型错误。是的,在计算机的底层我们是没有办法直接操作两个内存单元之间的交互(mov,add)。我们原本所认为的*ap = (*ap) + (*bp);其实在转为汇编的时候是这样的。

mov ax, [bx]            ; 将ds:bx的内容传输到ax中
add ax, [bp]            ; 将ss:bp的内容与ax中的内容相加结果保存在ax中

0x4 最后

不得不承认的一件事是,随着编程语言的进步,程序员能在越来越短的时间内做越来越多的事。这是一种进步,类似Python能让数据操作变得非常简单。我很喜欢Python,它的确帮我节约了很多时间,但是我觉得如果我永远只停留在软件层,我会对我朝夕相处的计算机越来越陌生。所以还是希望以后的编程爱好者,在学习更多更丰富的高级语言的同时,多去了解一天之中与你相处最长时间的计算机。

最后祝大家身体健康、学习进步、工作顺利!



* 如果你对文章有任何意见或建议请发 邮件 给我!
* if you have any suggestion that you could send a E-mail to me, Please!