AsmEx - HsuJv/Note GitHub Wiki

Chapter 06:

> 程序如下, 编写code段中的代码, 将a段和b段的数据依次相加, 将结果存到d段中
assume cs : code

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

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

d segment
    db 0, 0, 0, 0, 0, 0, 0, 0
d ends

code segment
start:
...
code ends
end start

思考: 80x86CPU中的段寄存器有且只有CS, DS, ES, SS四个, 显然CS, SS不能用来指向abd三个段中的任意一个, 则可用寄存器仅两个(DS, ES)

在不改变源数据的情况下, 则最简单的解决方案就是先通过8次循环将数据段a中的数据复制到d段中

    mov cx, 8   ; loop for 8 times
s1:
    xor ax, ax
    mov al, es:[bx]
    mov [bx], al
    inc bx
    loop s1      ; end loop1
                 ; 数据段a的内容被送入d

接着, 再进行8次循环, 一次使用 add 指令将b段数据与d段数据相加, 结果保存在d段

    mov cx, 8
s2:
    xor ax, ax
    mov al, es:[bx]
    add [bx], al
    inc bx
    loop s2       ; end loop2

详细代码在Repository-Note中可见

Return To Notes

Chapter 08:

> Power idea 公司从1975年成立一直到1995年的基本情况如下
> 
> 年份    收入(千美元)     雇佣(人)       人均收入(千美元)
> 1975      16              3                   ?
> 1976      22              7                   ?
> 1977      38              9                   ?
> ...
> 1995      5937000         17800               ?
> 
> 下面的程序中, 已经定义好了这些数据
assume cs:codesg, ds:datasg, ss:stacksg

datasg 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年
    dd      16, 22, 382, 1356, 2390, 8000, 16000, 24486, 50065 
    dd      97479, 140417, 197514, 345980, 590827, 803530, 1183000, 1843000, 2759000
    dd      3753000, 4649000, 5937000    ; 21年内的总收入
    dw      3, 7, 9, 12, 28, 38, 130, 220, 476
    dw      778, 1001, 1442, 2258, 2793, 4037, 5635, 8226, 11542
    dw      14430, 15257, 17800  ; 21年雇佣员工数
datasg ends

table segment
    db      21  dup('year summ ne ?? ')
table ends
> 编程, 将data段中的数据按如下格式写入到table段中, 并计算21年中的人均收入(
> 取整), 结果也按照下面的格式保存在table段中

>            |  年份(4B) | 空| 收入(4B) | 空|雇员数(2B)|空|人均收入(2B)| 空
> 行内地址   |  0 1 2 3  | 4 |  5 6 7 8 |  9|  A    B  | C|   D    E   | F
> 行首地址   |           |   |          |   |          |  |            |   
> table:0    | '1 9 7 5' |   |     16   |   |    3     |  |     ??     |
> table:10H  | '1 9 7 6' |   |     22   |   |    7     |  |     ??     | 

思考: 题目中有三个数据段要复制, 一个算术结果要保存;经观察发现, 第一段数据和第二段数据, 每两个值之间的地址偏移量都是4, 第三段数据之间的偏移是2

考虑实现: 用bx指向数据段起始, si指向收入段起始, di指向雇员人数起始, es指向目标段, bp指向当前要复制的位置, 然后开始循环21次

    mov     ax, datasg
    mov     ds, ax
    mov     ax, table
    mov     es, ax
    mov     ax, stacksg
    mov     ss, ax
    mov     sp, 6
    mov     si, 84      ; 指向总收入
    mov     di, 168     ; 指向总人数
    mov     bp, 0       ; bp = 0, 用于指向要复制到的内存单元
    mov     cx, 21      ; 循环21次
l1: 
    ...

每次循环中嵌套一个循环复制年份, 进行二重循环时需要使用栈保存循环中要用到的寄存器的值, 再循环结束后返回

l2:
    mov     al, [bx+si]
    mov     es:[bp+si], al
    inc     si
    loop    l2      ; l2完成年份的复制

计算人均收入的时候, 先把si指向的目标单元低两字节通过ax转移到目标地址中, 再把高两字节用dx转移, 接着直接用div word ptr [di]计算人均收入, 因为结果取整, 所以直接复制ax的值即可。

在循环的最后, 对bx, si, di分别增加4, 4, 2, 并把bp指向目标段下一行的起始

详细代码在Repository-Note中可见

Return To Notes

Chapter 09-1:

> 分析下面程序, 在运行前思考, 这个程序可以正确返回吗?
> 运行后思考, 为什么会是这种结果?
assume cs:codesg
codesg segment
    mov     ax, 4c00h
    int     21h
start:
s:
    nop
    nop
    mov     di, offset s
    mov     si, offset s2
    mov     ax, cs:[si]
    mov     cs:[di], ax     

s0:
    jmp     short   s

s1:
    mov     ax, 0
    int     21h
    mov     ax, 0

s2:
    jmp     short   s1
    nop
codesg ends
end start

思考: 因为编译器在编译程序时就会根据offset给出的标号计算出偏移量, 所以 mov cs:[di], ax 复制给cs:[offset s]的开头的指令并不是 jmp short s1 而是 jmp short offset s2 - offset s1 从而导致程序跳转到s标号后, ip的值继续变小, 接着执行代码段最开始的返回指令

Return To Notes

Chapter 09-2:

> 编程。在屏幕中间分别显示绿色, 绿底红色, 白底蓝色的字符串'Welcome to 
> masm!'
> 
> 编程所需的知识通过阅读, 分析下面的材料获得
> 80x25彩色字符模式显示缓冲区的结构: 
> 
> 内存地址空间中, b8000h-bffffh共32KB的空间, 为80x25彩色字符模式显示 缓冲区
> 向这个地址空间写入数据, 写入的数据, 写入的内容会立即出现在显示器上
> 
> 在80x25彩色字符模式下, 显示器可以显示25行, 每行80个字符, 每个字符可以有
> 256种属性(背景色, 前景色, 闪烁, 高亮等组合信息)
> 
> 这样, 一个字符在显示缓冲区中要占两个字节, 分别存放字符的ASCII码(低地址)
> 和属性(高地址), 80x25模式下, 一屏的内容在显示缓冲区共占4000个字节
> 
> 显示缓冲区分为8页, 每页4KB≈4000B。显示器可以显示任意一页的内容, 一般情况
> 下, 显示第0页(b8000h-b8f9f)的内容
> 
> 属性字节的格式: 
>           7   6   5   4   3   2   1   0
> 含义     BL   R   G   B   I   R   G   B
>          闪烁    背景    高亮    前景

思考: 通过es定位到b800段, 通过偏移160*12+40*2定位到第13行第41个字符的位置。用ds指向字符串, 将ah赋值为想要的颜色属性对应代码, 每次将当前字符送入al, 最后将ax送入显示缓冲区

详细代码在Repository-Note中可见

Return To Notes

Chapter 10-1:

> 编写一个通用的子程序来实现显示字符串的功能
> 子程序描述: 
> 功能: 在指定的位置, 用指定的颜色, 显示一个用0结束的字符串
> 参数: (dh) = 行号, (dl) = 列号
>       (cl) = 颜色, ds:si指向字符串的首地址
> 返回: 无

思考: 在子程序中, 首先通过dh和dl定位到b800段中相应的偏移处

    xor     ax, ax  ; ax置0
    mov     al, dh  ; 得到dh的值
    sub     ax, 1   
    mov     bl, 0a0h
    mul     bl      ; 计算出第dh行0列相对b800:0的偏移
    xor     dh, dh
    add     dx, dx
    add     ax, dx      
    sub     ax, 2   ;要打印的第一个字符的偏移

接着, cx置零, 开始循环复制, 在循环中不停的将源字符串的值送入cl, 再送入al, ah被送入颜色代码, 最后将ax送入显示缓冲区, 当字符串尾的0被送入cl时, cx = 0, 退出循环。

    xor     cx, cx
trans:      
    mov     cl, [si]
    mov     al, [si]
    mov     es:[bx], ax
    add     bx, 2
    inc     si
    inc     cx
    loop    trans
    ret

详细代码在Repository-Note中可见

Return To Notes

Chapter 10-2:

> 编写一个通用的子程序来实现不会溢出的除法运算, 被除数为dword型, 除数为word
> 型, 结果为dword型
> 参数: (ax) = dword型数据的低16位
>       (dx) = dword型数据的高16位
>       (cx) = 除数
> 返回: (dx) = 商的高16位
>       (ax) = 商的低16位
>       (cx) = 余数

思考: 首先, 先对高16位进行除法运算, 此时低16位的值被保存入栈

    push    ax
    mov     ax, dx
    xor     dx, dx
    div     cx

执行完上述代码后, ax里是商的高16位, dx里是高16位里的余数, 应该和原低16合为一个新的dword数据, 分别放入dx, ax, 此次除法运算肯定不会溢出

    pop     bx
    push    ax
    mov     ax, bx
    div     cx

最后, 将余数送入cx, 用之前压入栈中的第一次除法的商覆盖dx

    mov     cx, dx
    pop     dx

详细代码在Repository-Note中可见

Return To Notes

Chapter 14:

> 在CMOS RAM中, 存放着当前时间:
> 秒: 00h
> 分: 02h 
> 时: 04h
> 日: 07h
> 月: 08h
> 年: 09h
> 这6个信息的长度都为1个字节
> 这些数据以BCD码的方式存放, 高4位存放十位上的数值, 低4位存放个位
> 70h为地址端口, 存放要访问的CMOS RAM单元的地址
> 71h为数据端口, 存放从选定的CMOS RAM单元中读取的数据或要写入的数据

思考: 这个程序主要做两部分工作

  • 从CMOS RAM的8号单元读取当前月份的BCD码
    mov     al, 08h
    out     70h, al
    in      al, 71h     ; 获得月份的bcd码
  • 将BCD码转换为对应的ASCII码
    1. 首先将BCD码复制ah中, 这样, ax中就存放着四位的BCD码
    2. 将ah的内容右移4位, 这样, ah中存放月份的十位的BCD码
    3. 将al和00001111b进行and操作, 这样, al中存放月份的个位的BCD码
    4. 最后将al和ah分别加上30h, 将BCD码转换为对应的ASCII码
    5. 调用21号中断进行输出

详细代码在Repository-Note中可见

Return To Notes

Chapter 15:

> 在屏幕中依次显示'a' - 'z', 并可以让人看清
> 在显示过程中, 按下Esc键后改变颜色

思考: 程序需要完成三个任务:

  1. 依次显示'a' - 'z'
  2. 使显示间隔肉眼可见
  3. 改写int 9号中断使得60端口接收到Esc键的扫描码时更改显示颜色

简单实现:

  • 定位到b800:[160*12+40*2], 使得字母显示到显示屏第13行第41列位置
  • 循环26次, 用一个寄存器存放'a'的ASCII码值, 每次循环中输出改寄存器, 每次循环完自增1
  • 每次循环中调用函数Delay使得延迟肉眼可见
delay:
    push    ax
    push    dx
    xor     ax, ax
    mov     dx, 1000h

de:
    sub     ax, 1
    sbb     dx, 0
    cmp     ax, 0
    jne     de
    cmp     dx, 0
    jne     de

    pop     dx
    pop     ax
    ret
  • 该函数通过10000h * 1000h次无意义的循环, 使得输出字符的两次循环间有长的延迟
  • 将原int 9号中断的cs:ip保存, 写入当前程序的数据段
    push    word ptr    es:[24h]
    push    word ptr    es:[26h]
    pop     word ptr    ds:[2]
    pop     word ptr    ds:[0]  
  • 接着用自定义的newInt 9子程序的起始地址覆盖中断向量表中原int 9号中断起始地址
    mov     word ptr    es:[24h], offset newInt9
    mov     word ptr    es:[26h], cs
  • 在自定义的newInt 9子程序中将标志寄存器中的TF, IF置0, 接着 call ds:[0] 调用原int 9号中断处理键盘输入
  • 然后对端口60h的内容进行读取, 并考察是否是Esc
  • 如果是Esc键的扫描码, 则将b800:[160*12+40*2+1]的值自增1, 实现颜色的改变
  • 否则, 直接iret结束此次中断
newInt9:
    push    ax
    push    bx
    push    es

    pushf
    pushf
    pop     bx
    and     bh, 11111100b
    push    bx
    popf        ; 将IF, TF置0
    call    dword ptr   ds:[0]

    in      al, 60h
    cmp     al, 1
    jne     newInt9re

    mov     ax, 0b800h
    mov     es, ax
    inc     byte ptr    es:[160*12+40*2+1]

newInt9re:
    pop     es
    pop     bx
    pop     ax
    iret

详细代码在Repository-Note中可见

Return To Notes

⚠️ **GitHub.com Fallback** ⚠️