03 内存与寻址 
1 内存 
DOS 系统在实模式下,能够访问 [00000h, 0FFFFFh] 共 1MB 的内存空间,这里的 12345h 称为物理地址,寄存器放不下,所以需要段地址 和偏移地址  
一个段的最大长度位 0FFFFh,也就是 64kB 
 
2 段地址、偏移地址 
段地址的 16 进制个位必须是 0 
段的长度为 2233Fh-12340h+1=10000h=64k 
一个物理地址可以表示为多个逻辑地址
12398h1230:00981239:00081234:0058
总之就是段地址只管到十位,偏移地址能更深入一位 
 
段地址 +1 等效于偏移地址 +10h
 
 
 
 
2.1 直接寻址和间接寻址 
2.1.1 直接寻址:使用常数来表示偏移地址 
间接寻址 mov   al ,   ds :[ 2000 h ] 
mov   al ,   byte   ptr   ds :[ 2000 h ]    ; 声明类型是字节指针 
                              ; 但其实可以省略,因为 al 就是一个字节 
上面的操作等价于:
间接寻址 typedef   unsigned   char   byte 
al   =   * ( byte   * )( ds : 2000 h ); 
2.2 间接寻址: 使用寄存器或寄存器 + 常数来表示偏移地址 
Attention
用于间接寻址的寄存器仅限  bx, bp, si, di,而且一定是 bx, bp 中的一个能和 si, di 中的一个相加
 
间接寻址 mov   bx ,   2000 h 
mov   al ,   ds :[ bx ] 
mov   al ,   byte   ptr   ds :[ bx ] 
mov   ds :[ bx ],   1    ; 语法错误!! 
mov   bypt   ptr   ds :[ bx ],   1 
mov   word   ptr   ds :[ bx ],   1 
mov   dword   ptr   ds :[ bx ],   1 
Warning
当源操作数是常数 ,目标操作数是变量 时,无法确定宽度,必须指定 ptr
 
3 段缺省和段覆盖 
引用数组元素 mov   ah ,   [ abc ]    ; 直接操作 abc 地址指向的对象 
                ; 默认指定了 byte ptr 
                ; 默认段地址就是 ds 
; 其完整形式为 
mov   ah ,   byte   ptr   ds :[ abc ] 
3.1 段缺省的三个原则 
直接寻址,则缺省 ds 
间接寻址,含有 bp 时,缺省 ss 
间接寻址,不含 bp 时,缺省 ds 
 
3.2 段覆盖 
强制使用类似 cs:[1000h] 的形式覆盖段缺省的默认值 
 
3.3 Assume 的作用 
帮助编译器建立寄存器与段的关联,当源程序引用了某个段内的变量时,编译器会自动将段地址替换为关联的段地址寄存器
4 1M 内存空间的划分 
地址范围 
用途 
大小 
 
 
[0000:0000, 9000:0000]操作系统和用户程序 
640K 
 
[A000:0000, A000:FFFF]映射显卡内存 图形模式  
64K 
 
[B000:0000, B000:7FFF]映射显卡内存 
32K 
 
[B800:0000, B800:7FFF]映射显卡内存 文本模式  
32K 
 
[C000:0000, F000:FFFF]映射 ROM 
320K 
 
 
5 寄存器总结 
5.1 16 位 CPU 中共有 14 个寄存器 
ax, bx, cx, dx, sp, bp, si, di
bx, bp, si, di 用来表示偏移地址 ,可以放在 [] 内ax, bx, cx, dx 称为通用寄存器 ,常用于算数、逻辑运算 
cs, ds, es, ss 用来表示段地址 
cs:ip 指向当前将要执行的指令,ip 是指令指针(instruction pointer),cs 是代码段寄存器ss:sp 指向堆栈顶端,其中 sp 是堆栈指针(stack pointer),ss 是堆栈段寄存器es 附加段寄存器,和 ds 一样,可以表示一个数据段的地址 
ip, fl 
5.2 堆栈的简单操作 
push and pop stk   segment   stack 
db   100 h   dup ( 0 ) 
stk   ends 
code   segment 
assume   cs : code 
main: 
     mov   ax ,   1234 h 
     mov   bx ,   5678 h 
     push   ax 
     push   bx 
     mov   ax ,   0 
     mov   bx ,   0 
     pop   bx 
     pop   ax 
code   ends 
end   main 
6 远指针、近指针 
6.1 lea 加载偏移地址 
加载偏移地址,load effective address
 
lea   dx ,   ds :[ bx ]    ; 相当于 mov dx, bx,并没有简化 
lea   dx ,   ds :[ bx + si + 3 ]    ; 相当于一次计算了两个加法,其他指令无法做到,有用 
lea   eax ,   [ eax + eax * 4 ]    ; EAX=EAX*5,用 lea 作乘法 
6.2 远指针 (Far Ptr) 
16 位汇编,xxxx:xxxx,即 16  位段地址 + 16  位偏移地址,dword ptr 
32 位汇编,xxxx:xxxxx,即 16  位段地址 + 32  位偏移地址,fword ptr 
 
Attention
小端规则 
远指针和 int 无法区分,需要由使用者定义 
 
 
6.3 les, lds 加载段地址 
将段地址加载到 es 或者 ds 中,将偏移地址加载到指定寄存器中,Load segment address to Extra Segment reg / Data Segment reg
 
取用远指针 ; bx=0, ds=1000h 
mov   di :   ds :[ bx ]    ; di=5678h 
mov   es ,   ds :[ bx + 2 ]    ; es=1234h 
lds   bx ,   ds :[ bx ]    ; ds=1234h, bx=5678h 
les   di ,   ds :[ bx ]    ; es=[31:16]=1234h, di=[15:0]=5678h 
Warning
常用 les,因为 ds 寄存器代表代码段,一般不修改
 
Example: 使用变量保存远指针 data   segment 
video_addr   dw   0000 h ,   0 B800h ,   160 ,   0 B800h    ; 上述定义也可以写成: 
                                           ; video_addr dd 0B8000000h, 0B80000A0h 
                                           ; video_addr db 00, 00, 00, 0B8h, 0A0h, 00, 00, 00, 0B8h 
data   ends 
code   segment 
assume   cs : code ,   ds : data 
main: 
    mov   ax ,   data 
    mov   ds ,   ax 
    mov   bx ,   0 
    mov   cx ,   2 
next: 
    les   di ,   dword   ptr   video_addr [ bx ]    ; es:di=B800:[0000] 
    mov   word   ptr   es :[ di ],   1741 h 
    add   bx ,   4 
    sub   cx ,   1 
    jnz   next 
    mov   ah ,   1 
    int   21 h 
    mov   ah ,   4 Ch 
    int   21 h 
code   ends 
end   main 
7 补充:32 位寻址方式 
[寄存器 + 寄存器*n + 常数]
n=1,2,4,8寄存器为 eax, ebx, ecx, edx, esi, edi, esp, ebp 从中任选一个,可以同名 限制更少了  
 
 
遍历数组 ; long int a[3]={10,20,30}; 
; assume ds=seg a, ebx=offset a, esi=0 
mov   ecx ,   3 
again: 
     mov   eax ,   ds :[ ebx + esi * 4 ] 
     add   [ sum ],   eax 
     add   esi ,   1 
     sub   ecx ,   1 
     jnz   again 
8 显卡地址映射 
8.1 文本模式:操作文本内容和颜色 
屏幕的左上角为原点,横向为 \(x\)  轴,纵向为 \(y\)  轴,右下角的坐标为 \((79, 24)\) ,也就是 80 格宽,25 格高 
offset = (y * 80 + x) * 2可以指定字符输出的位置和颜色 
 
8.1.1 数据格式 
每 2 byte 决定屏幕上的一个字符,分别对应 [ASCII Code, Color],颜色的高 4 位是背景色,低 4 位是前景色 
颜色对照表如下,红绿蓝可以合成其他颜色 
 
背景 
前景 
 
 
位 
7 
6 
5 
4 
3 
2 
1 
0 
 
对应颜色元素 
闪烁 
红 
绿 
蓝 
高亮 
红 
绿 
蓝 
 
 
8.1.2 Example 
左上角显示红色的 A 和绿色的 B mov   ax ,   0 B800h 
mov   ds ,   ax 
mov   byte   ptr   ds :[ 0 ],   ' A ' 
mov   byte   ptr   ds :[ 1 ],   74 h 
mov   byte   ptr   ds :[ 2 ],   ' B ' 
mov   byte   ptr   ds :[ 3 ],   72 h 
Hint
0B800h 可以认为是显卡的地址,往显卡写入字符就可以显示在屏幕上74h 中,7 表示背景色是白色,4 用来表示前景色是红色 
 
文本模式下用 * 显示汉字“我” data   segment 
hz   db   04 h , 80 h , 0 Eh , 0 A0h , 78 h , 90 h , 08 h , 90 h 
    db   08 h , 84 h , 0 FFh , 0 FEh , 08 h , 80 h , 08 h , 90 h 
    db   0 Ah , 90 h , 0 Ch , 60 h , 18 h , 40 h , 68 h , 0 A0h 
    db   09 h , 20 h , 0 Ah , 14 h , 28 h , 14 h , 10 h , 0 Ch 
data   ends 
code   segment 
assume   cs : code ,   ds : data 
main: 
    mov   ax ,   data 
    mov   ds ,   ax 
    mov   ax ,   0 B800h 
    mov   es ,   ax 
    mov   ax ,   0003 h 
    int   10 h 
    mov   di ,   0 
    mov   dx ,   16 
    mov   si ,   0 
next_row: 
    mov   ah ,   hz [ si ] 
    mov   al ,   hz [ si + 1 ] 
    add   si ,   2 
    mov   cx ,   16 
check_next_dot: 
    shl   ax ,   1 
    jnc   no_dot 
is_dot: 
    mov   byte   ptr   es :[ di ],   ' * ' 
    mov   byte   ptr   es :[ di + 1 ],   0 Ch 
no_dot: 
    add   di ,   2 
    sub   cx ,   1 
    jnz   check_next_dot 
    sub   di ,   32 
    add   di ,   160 
    sub   dx ,   1 
    jnz   next_row 
    mov   ah ,   1 
    int   21 h 
    mov   ah ,   4 Ch 
    int   21 h 
code   ends 
end   main 
8.2 图形模式:操作像素点 
调用 int 10h 中断,将显卡切换到 320*200,256 色的图形模式
mov   ah ,   0    ; 调用 int 10h 的 0 号子功能 
mov   al ,   13 h    ; 代表图形模式编号 
int   10 h 
8.2.1 坐标转换 
(x, y) -> y*320+x
8.2.2 数据格式 
0 
1 
2 
3 
4 
5 
6 
7 
 
 
黑 
蓝 
绿 
青 
红 
洋红 
棕 
白 
 
8 
9 
A 
B 
C 
D 
E 
F 
 
灰 
亮蓝 
亮绿 
亮青 
亮红 
紫 
黄 
亮白 
 
 
```asm title="进入图形模式并显示红色方块" hl=
code segment
assume cs:code; cs不需要赋值会自动等于code
main:
   jmp begin
i  dw 0
begin:
   mov ax, 0013h
   int 10h
   mov ax, 0A000h
   mov es, ax
   ;(320/2, 200/2)
   mov di, (100-20)320+(160-20); (160-20,100-20)
   ;mov cx, 41; rows=41
   mov i, 41
next_row:
   ;push cx
   push di
   mov al, 4; color=red
   mov cx, 41; dots=41
next_dot:
   mov es:[di], al
   add di, 1
   sub cx, 1
   jnz next_dot
   pop di; 左上角(x,y)对应的地址
   ;pop cx; cx=41
   add di, 320; 下一行的起点的地址
   ;sub cx, 1; 行数-1
   sub i, 1
   jnz next_row
   mov ah,0
   int 16h;bios键盘输入,类似int 21h的01h功能
   mov ax, 0003h
   int 10h; 切换到80 25文本模式
   mov ah, 4Ch
   int 21h
code ends
end main
9 端口 
Note
CPU <-> 端口 (port) <-> I/O 设备
 
端口编号就是端口地址,[0000h, 0FFFFh] 一共有 65536 个端口
port operation in   al ,   60 h    ; 从 60h 端口获得输入 
out   60 h ,   al    ; 将 al 的值输出到 60h 端口  
9.1 Example: 读取键盘输入值 
60h 端口是键盘的输入端口,通过修改硬件断点 int 9h 函数指针,让程序在键盘敲击时读取键盘输入值
9.1.1 Code %% fold %% 
key ;--------------------------------------- 
;PrtSc/SysRq: E0 2A E0 37 E0 B7 E0 AA  ; 
;Pause/Break: E1 1D 45 E1 9D C5        ; 
;--------------------------------------- 
data   segment 
old_9h   dw   0 ,   0 
stop     db   0 
key      db   0 ; key=31h 
phead    dw   0 
key_extend    db   ' KeyExtend =' ,   0 
key_up        db   ' KeyUp =' ,   0 
key_down      db   ' KeyDown =' ,   0 
key_code      db   ' 00 h   ' ,   0 
hex_tbl       db   ' 0123456789 ABCDEF ' 
cr            db    0 Dh ,   0 Ah ,   0 
data   ends 
code   segment 
assume   cs : code ,   ds : data 
main: 
    mov   ax ,   data 
    mov   ds ,   ax 
    xor   ax ,   ax 
    mov   es ,   ax 
    mov   bx ,   9 * 4 
    push   es :[ bx ] 
    pop   old_9h [ 0 ] 
    push   es :[ bx + 2 ] 
    pop   old_9h [ 2 ]      ; 保存int 9h的中断向量 
    cli 
    mov   word   ptr   es :[ bx ],   offset   int_9h 
    mov   es :[ bx + 2 ],   cs ; 修改int 9h的中断向量 
    sti 
again: 
    cmp   [ stop ],   1 
    jne   again          ; 主程序在此循环等待 
    push   old_9h [ 0 ] 
    pop   es :[ bx ] 
    push   old_9h [ 2 ] 
    pop   es :[ bx + 2 ]      ; 恢复int 9h的中断向量 
    mov   ah ,   4 Ch 
    int   21 h 
int_9h: 
    push   ax 
    push   bx 
    push   cx 
    push   ds 
    mov   ax ,   data 
    mov   ds ,   ax         ; 这里设置DS是因为被中断的不一定是我们自己的程序 
    in   al ,   60 h         ; AL=key code 
    mov   [ key ],   al 
    cmp   al ,   0 E0h 
    je    extend 
    cmp   al ,   0 E1h 
    jne   up_or_down 
extend: 
    mov   [ phead ],   offset   key_extend 
    call   output 
    jmp   check_esc 
up_or_down: 
    test   al ,   80 h       ; 最高位==1时表示key up 
    jz   down 
up: 
    mov   [ phead ],   offset   key_up 
    call   output 
    mov   bx ,   offset   cr 
    call   display       ; 输出回车换行 
    jmp   check_esc 
down: 
    mov   [ phead ],   offset   key_down 
    call   output 
check_esc:     
    cmp   [ key ],   81 h     ; Esc键的key up码 
    jne   int_9h_iret 
    mov   [ stop ],   1 
int_9h_iret: 
    mov   al ,   20 h        ; 发EOI(End Of Interrupt)信号给中断控制器, 
    out   20 h ,   al        ; 表示我们已处理当前的硬件中断(硬件中断处理最后都要这2条指令)。 
                     ; 因为我们没有跳转到的old_9h,所以必须自己发EOI信号。 
                     ; 如果跳到old_9h的话,则old_9h里面有这2条指令,这里就不要写。 
    pop   ds 
    pop   cx 
    pop   bx 
    pop   ax 
    iret               ; 中断返回指令。从堆栈中逐个弹出IP、CS、FL。 
output: 
    push   ax 
    push   bx 
    push   cx 
    mov   bx ,   offset   hex_tbl 
    mov   cl ,   4 
    push   ax     ; 设AL=31h=0011 0001 
    shr   al ,   cl ; AL=03h 
    xlat        ; AL = DS:[BX+AL] = '3' 
    mov   key_code [ 0 ],   al 
    pop   ax 
    and   al ,   0 Fh ; AL=01h 
    xlat         ; AL='1' 
    mov   key_code [ 1 ],   al 
    mov   bx ,   [ phead ] 
    call   display       ; 输出提示信息 
    mov   bx ,   offset   key_code 
    call   display       ; 输出键码 
    pop   cx 
    pop   bx 
    pop   ax 
    ret 
display: 
    push   ax 
    push   bx 
    push   si 
    mov   si ,   bx 
    mov   bx ,   0007 h      ; BL = color 
    cld 
display_next: 
    mov   ah ,   0 Eh        ; AH=0Eh, BIOS int 10h的子功能,具体请查中断大全 
    lodsb 
    or   al ,   al 
    jz   display_done 
    int   10 h            ; 每次输出一个字符 
    jmp   display_next 
display_done: 
    pop   si 
    pop   bx 
    pop   ax 
    ret 
code   ends 
end   main 
9.1.2 Explain 
9.2 Example: 读取 CMOS 时钟 
70h, 71h 与 CMOS 时钟有关,其中的 4, 2, 0 分别表示当前的时分秒,使用 BCD 码
mov   al ,   2 
out   70 h ,   al    ; 70h 端口收到 2 号地址会将地址 2 和 71h 端口连通 
mov   al ,   10 h 
out   71 h ,   al    ; 将 10h 写入 2 号地址 
in   al ,   71 h    ; 读取 2 号地址的值 
BCD to char convert: 
     mov   ah ,   al    ; assume ah=al=19h 
     and   ah ,   0 Fh    ; ah=9h 
     shr   al ,   4    ; al=1h 
     add   ah ,   ' 0 '    ; ah='9' 
     add   al ,   ' 0 '    ; al='1' 
     ret 
9.2.1 Code %% fold %% 
readtime data   segment 
current_time   db   " 00 : 00 : 00 " ,   0 Dh ,   0 Ah ,   ' $ ' 
data   ends 
code   segment 
assume   cs : code ,   ds : data 
main: 
    mov   ax ,   data 
    mov   ds ,   ax 
    mov   al ,   4 
    out   70 h , al ; index hour 
    in   al , 71 h   ; AL=hour(e.g. 19h means 19 pm.) 
    call   convert ; AL='1', AH='9' 
    ;mov word ptr current_time[0],ax 
    mov   current_time [ 0 ],   al 
    mov   current_time [ 1 ],   ah 
    mov   al , 2 
    out   70 h , al ; index minute 
    in    al , 71 h ; AL=minute 
    call   convert 
    mov   word   ptr   current_time [ 3 ], ax ; 
    ;mov current_time[3], al 
    ;mov current_time[4], ah 
    mov   al , 0    ; index second 
    out   70 h , al 
    in    al , 71 h ; AL=second 
    call   convert 
    mov   word   ptr   current_time [ 6 ], ax 
    mov   ah ,   9 
    mov   dx ,   offset   current_time 
    int   21 h 
    mov   ah ,   4 Ch 
    int   21 h 
;---------Convert---------------- 
;Input:AL=hour or minute or second 
;      format:e.g. hour   15h means 3 pm. 
;                  second 56h means 56s 
;Output: (e.g. AL=56h) 
;     AL='5' 
;     AH='6' 
convert: 
     push   cx 
     mov   ah , al   ; e.g. assume AL=56h 
     and   ah , 0 Fh ; AH=06h 
     mov   cl , 4 
     shr   al , cl   ; AL=05h 
     ; shr:shift right右移 
     add   ah ,   ' 0 ' ; AH='6' 
     add   al ,   ' 0 ' ; AL='5' 
     pop    cx 
     ret 
;---------End of Convert--------- 
code   ends 
end   main 
9.3 几种读取键盘方式总结 
mov ah, 1; int 21h 相当于 al = getchar(),不能读取方向键 dos 中断调用 mov ah, 0; int 16h 相当于 ax = 键盘编码,可以读取方向键、PgUp 等,但不能读取单独的方向键 bios (basic I/O system) 调用 
bios 封装在 ROM 芯片中,可以在没有操作系统的情况下调用 
 
in al, 60h 可以读取所有按键的编码 端口操作