写这个系列文章的主要目的是记录书中重要的知识点,并和大家分享一些个人理解与实践。由于笔记中的知识点比较零散,而书中系统的介绍了一个 x86-16 处理器在实模式下的工作原理以及如何使用汇编语言与其进行“沟通”,所以推荐想要系统学习的朋友们去学习这本书。当我们掌握了实模式的工作原理之后,就可以进一步研究后来出现的其他运行模式(如保护模式)。除此之外,熟悉汇编语言有助于我们掌握上层语言(如 C)的执行原理,因为它们都要对汇编(机器码)进行抽象,而汇编程序就是基于 CPU 的执行机理写出来的。

第四章

一个有意义的汇编程序中至少要有一个段,即:代码段。在 masm 中可以用 assume 将有特定用途的段特定的段寄存器关联起来。

汇编程序从写出到执行的过程

编程 -> 1.asm -> 编译 -> 1.obj -> 连接 -> 1.exe -> 加载 -> 内存中的程序 -> 运行

所有语言编写的代码,都要经过类似方式最终被转换成计算机可以识别的机器码。比如:C 语言在编译系统(gcc,as,ld)中的处理过程是这样的:
编程 -> 1.c -> 预处理 -> 1.i -> 编译 -> 1.s -> 汇编 -> 1.o -> 链接 -> a.out

EXE 文件中程序的加载过程

参见:图 4.20(与 Linux 加载执行可执行文件(ELF)的过程类似)

实验三:编程、编译、连接、跟踪

  1. 下载 edit 编辑器(edit.com),将其拷贝至可执行程序目录 ~/DOS/bin
  2. ~/DOS/mytest/ 目录中使用 edit 编辑以下内容,然后保存到 t1.asm 文件中

    assume cs:codesg
    
    codesg segment
      mov  ax, 2000H
      mov  ss, ax
      mov  sp, 0
      add  sp, 10
      pop  ax
      pop  bx
      push ax
      push bx
      pop  ax
      pop  bx ; 这 4 行实现了交换 ax、bx 中的数据
    
      ; 程序返回
      mov  ax, 4c00H
      int  21H
    codesg ends
    
    end
  3. 编译、连接

    • 下载 masm5.0 汇编器(masm.exe) 和 Overlay Linker3.60 连接器(link.exe),将其拷贝至 ~/DOS/bin 目录下
    • 启动 dosbox, 使用 编译器连接器 进行编译和连接

      cd mytest
      
      ###### 编译 (参见4.4节) ######
      masm
      # 根据提示输入
      Source filename [.ASM]: c:\mytest\t1
      Object filename [ti.OBJ]: c:\mytest\t1
      Source listing  [NUL.LST]: [按下回车]
      Cross-reference [NUL.CRF]: [按下回车]
      
      ###### 连接 (参见4.5节) ######
      link
      # 根据提示输入
      Object Modules [.OBJ]: t1
      Run File [T1.EXE]: [按下回车]
      List File [NUL.MAP]: [按下回车]
      Libraries [.LIB]: [按下回车]
    • 编译、连接的简化方式:

      masm c:\mytest\t1
      link t1
  4. 执行和跟踪

    # 执行(加不加 .exe 后缀都可以)
    t1
    t1.exe
    
    # 跟踪(注:必须要加 .exe 后缀)
    debug t1.exe # 注意:要使用 -p 调试 "int 21H" 指令!!!

第五章

要完整的描述一个内存数据,需要的信息

  • 地址(即首地址、起始地址)
  • 类型(即上下文,可以确定该信息由多少个连续的存储单元组成,以及该内存区域的布局情况

cx 寄存器和 loop 指令实现循环

  1. cx 中存放循环次数
  2. loop 指令中的标号所标识的地址要在前面
  3. 循环执行的程序段写在标号和 loop 指令之间

loop 指令的内部执行过程

  • 先进行 cx=cx-1, 然后判断 cx 的值是否等于 0

    • 是:执行下一条指令(在取址阶段之后~执行 loop 之前,会将 IP 更新为 IP+loop指令长度
    • 否:继续执行循环体(在取址阶段之后~执行 loop 之前,会将 IP 更新为 标号代表的地址)

其他内容

5.4节 说明了 masm 编译器将 mov ax,[0] 识别为 mov ax,0 的解决办法——显示指定段前缀

注意,我们在纯 DOS 方式(实模式)下,可以不理会 DOS, 直接用汇编语言去操作真实的硬件,因为运行在 CPU 实模式下的 DOS, 没有能力对硬件系统进行全面、严格的管理。但在 Windows 2000、Unix 这些运行于 CPU 保护模式下的操作系统中,不理会操作系统,用汇编语言去操作真实的硬件,是根本不可能的。硬件已被这些操作系统利用 CPU 保护模式所提供的功能全面而严格的管理了。

DOS 中 0:200~0:2ff 这 256 个字节的空间一般是安全的。

实验四:[bx]和loop的使用

练习(2): 向 0:200~0:23F 内存单元内,依次写入十进制数字:0~63

; file: exp4_2.asm
assume cs:codesg

codesg segment
    mov ax, 0020H ; 0020:0~0020:3F 与 0:200~0:23F 物理地址重合,只是表示不同的段
    mov ds, ax    ; 使用 0020 作为段基址可以让【偏移量】和【要写入的数字】保持一致

    mov bx, 0
    mov cx, 64
 s: mov [bx], bl  ; 注意:1.每次写入一字节 2.`mov [dx], bl` 报错(见寄存器寻址规则)
    inc bx
    loop s

    mov ax, 4c00H
    int 21H
codesg ends

end

练习(3): 将 mov ax, 4c00H 指令前的指令复制到 0020:0000 开始的内存单元中

; file: exp4_3.asm
assume cs:codesg

codesg segment
    mov ax, cs
    mov ds, ax
    mov ax, 0020H
    mov es, ax
    mov bx, 0
    mov cx, 17H ; 如何知道要复制的指令字节长度是 "17H"? 是使用 debug 调试得到的
 s: mov al, [bx]
    mov es:[bx], al
    inc bx
    loop s

    mov ax, 4c00H
    int 21H
codesg ends

end

第六章

可执行文件的组成

  • 描述信息(汇编器、连接器对源程序中相关伪指令进行处理所得到的),如程序的入口地址
  • 程序(数据、指令)

6.1节 在代码段中使用数据,程序框架:

assume cs:codesg

codesg segement
        数据
        …………
 start: 代码
        …………
codesg ends

end start

将数据、代码、栈放入不同段的意义在于:

  1. 让程序的结构清晰明了,提高程序的可读性
  2. 让每个段独占 64KB 大小的地址空间(实现段隔离)
  3. 内存对齐,方便寻址,有利于代码优化:对于每个段,起始地址的偏移地址都是 0000, 而将数据全部定义在代码段中时,数据会依次紧凑排列(不会对齐)

实验五:编写、调试具有多个段的程序

练习(5): 将 a 段和 b 段中的数据同一列相加,结果存入 c 段对应列中(实现方法一)

; file: exp5_5.asm
assume cs:code

a segment
    dw 1,2,3,4,5,6,7,8
a ends

b segment
    dw 1,2,3,4,5,6,7,8
b ends

c segment
    dw 0,0,0,0,0,0,0,0
c ends
; 程序退出前,通过 "-d 076c:0 2f" 能够看到以上3个段实际的内存数据(虽然是3个段,但它们占用的内存是连续的)

code segment
start: mov bx, 0
       mov cx, 8

    s: mov ax, a
       mov ds, ax
       mov dx, ds:[bx]  ; 获取 a 段的值,存入 dx
       
       mov ax, b
       mov ds, ax
       add dx, ds:[bx]  ; 获取 b 段的值,计算加法,存入 dx

       mov ax, c
       mov ds, ax
       mov ds:[bx], dx  ; dx 写入 c 段中

       ;add bx, 2
       inc bx
       inc bx
       loop s

       mov ax, 4c00H
       int 21H
code ends

end start

练习(5): 实现方法二——使用寄存器+偏移量进行寻址

; file: exp5_5_1.asm
assume cs:code

a segment
    db 1,2,3,4,5,6,7,8
a ends

b segment
    db 8,7,6,5,4,3,2,1
b ends

c segment
    db 0,0,0,0,0,0,0,0
c ends
; 程序退出前,通过 "-d ds:0 2f" 能够看到以上3个段实际的内存数据(虽然是3个段,但它们占用的内存是连续的)

code segment
start: mov ax, a
       mov ds, ax

       mov bx, 0
       mov cx, 8

    s: mov dx, ds:[bx]  
       add dx, ds:[bx+16]
       mov ds:[bx+32], dx

       inc bx
       loop s

       mov ax, 4c00H
       int 21H
code ends

end start

练习(6): 用 push 指令将 a 段中前 8 个字型数据,逆序存储到 b 段中

; file: exp5_6.asm
assume cs:code

a segment
    dw 1,2,3,4,5,6,7,8,9,0aH,0bH,0cH,0dH,0eH,0fH,0ffH
a ends

b segment
    dw 0,0,0,0,0,0,0,0
b ends

code segment
start: mov ax, a
       mov ds, ax

       mov ax, b
       mov ss, ax
       mov sp, 10H

       mov bx, 0
       mov cx, 8
    s: push ds:[bx]
       ;add bx, 2
       inc bx
       inc bx
       loop s

       mov ax, 4c00H
       int 21H
code ends

end start

第七章

在汇编程序中,可以用单引号引用字符,编译器将把它们转换为相应的 ASCII 码。如:

db 'unIX'
; 相当于
db 75H, 6EH, 49H, 58H

mov al, 'a'
; 相当于
mov al, 61H

大小写字母转换问题

对于字母的 ASCII 码,有以下特征:

  1. 所有字母 [a-zA-Z] 的 ASCII 码值,二进制的最高位1都在第7位,即可表示为:01** ****
  2. 同一字母,大写码值+20H=小写码值即相差 32 (2^5)

基于以上特征得出结论:对于同一字母的大小写码值,它们的 8 位二进制中,只有第6位有差异,其它位是相同的,即:

  • 大写字母的特征:ASCII 码值(十进制)为 65~90, 第 6 位一定是:0

    • 小写字母转大写:将第6位变成 0 即可,如:

      mov al, 'a'
      and al, dfH  ; 二进制:11011111B
  • 小写字母的特征:ASCII 码值(十进制)为 97~122, 第 6 位一定是:1

    • 大写字母转小写:将第6位变成 1 即可,如:

      mov al, 'A'
      or al, 00100000B

以下几种寻址方式,作用相同

; case1——[bx+idata]
mov ax, [bx+200]
mov ax, [200+bx]
mov ax, 200[bx]
mov ax, [bx].200

; case2——[bx+si]或[bx+di]
mov ax, [bx+si]
mov ax, [bx][si]

; case3——[bx+si+idata]或[bx+di+idata]
mov ax, [bx+si+200]
mov ax, [bx+200+si]
mov ax, [200+bx+si]
mov ax, 200[bx][si]
mov ax, [bx].200[si]
mov ax, [bx][si].200

问题 7.6(对 [bx(或 si、di)+idata] 的应用)

; 将 datasg 段中每个单词的头一个字母改为大写字母
assume cs:codesg,ds:datasg

datasg segment
  db '1. file         '  ; 16 bytes
  db '2. edit         '
  db '3. search       '
  db '4. view         '
  db '5. options      '
  db '6. help         '
datasg ends

codesg segment
start: mov ax, datasg
       mov ds, ax
       mov bx, 0
       mov cx, 6
    s: mov al, [bx+3]
       and al, 11011111B  ; 小写转大写,使用与运算将第 6 位设置为 0, 其他位不变
       mov [bx+3], al
       add bx, 16
       loop s

       mov ax, 4c00H
       int 21H
codesg ends

end start

问题 7.7(对 [bx+si(或 di)] 的应用)

; 将 datasg 段中每个单词改为大写字母
assume cs:codesg,ds:datasg

datasg segment
  db 'ibm             '  ; 16 bytes
  db 'dec             '
  db 'dos             '
  db 'vax             '
datasg ends

codesg segment
start: mov ax, datasg
       mov ds, ax
       mov bx, 0
       mov cx, 4
   s0: mov dx, cx ; 寄存器数量是有限的,当寄存器都被使用时,需要使用内存(通常是使用"栈",避免程序中定义额外的结构)来做临时存储区域

       mov si, 0
       mov cx, 3
    s: mov al, [bx+si]
       and al, 11011111B
       mov [bx+si], al
       INC si
       loop s

       add bx, 16
       mov cx, dx
       loop s0

       mov ax, 4c00H
       int 21H
codesg ends

end start

实验六——对问题 7.9 进行编程:将 datasg 段中每个单词的前4个字母改为大写字母

; file: exp6.asm
assume cs:codesg,ds:datasg,ss:stacksg

stacksg segment
  dw 0, 0, 0, 0, 0, 0, 0, 0
stacksg ends

datasg segment
  db '1. display      '
  db '2. brows        '
  db '3. replace      '
  db '4. modify       '
datasg ends

codesg segment
start: mov ax, stacksg
       mov ss, ax
       mov sp, 16
       mov ax, datasg
       mov ds, ax

       mov bx, 0
       mov cx, 4
   s0: push cx

       mov si, 0
       mov cx, 4
    s: mov al, [bx+3+si]
       and al, 11011111B
       mov [bx+3+si], al
       INC si
       loop s

       add bx, 16
       pop cx
       loop s0

       mov ax, 4c00H
       int 21H
codesg ends

end start

第八章——之前章节的总结

8086 中寄存器寻址的规则

偏移地址(寄存器的有效组合)

  1. 在 8086 中只有 bx, si, bi, bp 这4个寄存器可以用在 "[ ]" 中来进行内存单元的寻址
  2. 这4个寄存器可以单个出现,或只能以如下组合出现(bx, bp 不能配合;si, di 不能配合):

    • bx + si
    • bx + di
    • bp + si
    • bp + di
  3. 以上所有的出现方式,都可以再和立即数 idata 进行加减

段地址

  1. 只要在 "[ ]" 中使用了寄存器 bp, 如果没有显示指定段地址,那么段地址就在 ss
  2. 如果在 "[ ]" 中没使用寄存器 bp, 并且没有显示指定段地址,那么段地址就在 ds

寻址方式总结

  • 直接寻址——[立即数]
  • 寄存器间接寻址——[bx|si|bi|bp]
  • 寄存器相对寻址——[bx|si|bi|bp+立即数]
  • 基址变址寻址——[bx|bp+si|bi]
  • 相对基址变址寻址——[bx|bp+si|bi+立即数]

8086 机器指令中要处理的数据的位置及如何表达这个位置

位置表达方式
CPU 内部——寄存器使用寄存器名,如:ax
CPU 内部——指令缓冲器使用立即数来表达
内存指令中给出偏移地址段地址在相应的段寄存器中

8.6节 C语言结构体与汇编的【寻址语法】类比

8.7节 8086 中除法的计算规则

汇编指令:div 除数(在内存或某个寄存器中),除数要么是 8 位,要么是 16 位

8位除数16位除数
被除数16位——在 ax32位——dx 存高16位、ax 存低16位
alax
余数ahdx

实验七:寻址方式在结构化数据访问中的应用

将 data 段中的数据按照书中指定的格式写入到 table 段中,并计算 21 年中的人均收入(取整),结果也按照格式保存在 table 段中

; file: exp7.asm
assume cs:codesg

data segment
  db '1975','1976','1977','1978','1979','1980','1981','1982','1983'
  db '1984','1985','1986','1987','1988','1989','1990','1991','1992'
  db '1993','1994','1995'
  ; 以上是表示这21个【年份】的21个字符串(84字节)

  dd 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514
  dd 345980,590827,803530,1183000,1843000,2759000,3753000,4649000,5937000
  ; 以上是表示这21年【每年总收入】的21个 dword 型数据(84字节)

  dw 3,7,9,13,28,38,130,220,476,778,1001,1442,2258,2793,4037,5635,8226
  dw 11542,14430,15257,17800
  ; 以上是表示这21年【每年雇员人数】的21个 word 型数据(42字节)
data ends

table segment
  db 21 dup ('year summ ne ?? ')  ; 16 bytes
  ; 相当于声明 21 个:db 'year summ ne ?? '
table ends

codesg segment
start: mov ax, data
       mov ds, ax
       mov ax, table
       mov es, ax

       mov si, 0   ;【每年雇员人数】数组中,元素的偏移地址(2字节递增)
                   ;【年份】和【每年总收入】这两个数组中,元素的偏移地址(4字节递增,等于 si*2)
       mov di, 0   ; table 段数据(结构体数组),每行开始的地址(每行表示一个结构体元素)
       mov cx, 21
    s: push si
       add si, si  ; si*2

       ;【年份】数组的起始地址(相对于 data 段的偏移量)是 0
       mov ax, [si]
       mov es:[di], ax
       mov ax, [si+2]
       mov es:[di+2], ax
       mov byte ptr es:[di+4], 20H  ; 空格(ASCII值)

       ;【总收入】数组的起始地址(相对于 data 段的偏移量)是 84
       mov ax, [84+si]    ; 低位字节写入 ax (注意:字节序!!)
       mov es:[di+5], ax
       mov dx, [84+si+2]  ; 高位字节写入 dx
       mov es:[di+7], dx
       mov byte ptr es:[di+9], 20H

       ;【雇员数】数组的起始地址(相对于 data 段的偏移量)是 168
       pop si
       div word ptr [168+si]  ; 先计算【人均收入】,商存入 ax
       mov dx, [168+si]       ; 再获取雇员数
       mov es:[di+10], dx
       mov byte ptr es:[di+12], 20H

       mov es:[di+13], ax  ; 人均收入
       mov byte ptr es:[di+15], 20H

       add si, 2
       add di, 16
       loop s

       mov ax, 4c00H
       int 21H
codesg ends

end start

zandy
1 声望0 粉丝