03 内存与寻址
1 内存
DOS 系统在实模式下,能够访问 [00000h, 0FFFFFh]
共 1MB 的内存空间,这里的 12345h
称为物理地址,寄存器放不下,所以需要段地址 和偏移地址
一个段的最大长度位 0FFFFh
,也就是 64kB
2 段地址、偏移地址
段地址的 16 进制个位必须是 0
段的长度为 2233Fh-12340h+1=10000h=64k
一个物理地址可以表示为多个逻辑地址
12398h
1230:0098
1239:0008
1234: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
; &p = 1234:5678
1000:0000 78
1000:0001 56
1000:0002 34
1000:0003 12
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
```asm title="用图像模式显示汉字"我""
data segment
hz db 04h,80h,0Eh,0A0h,78h,90h,08h,90h
db 08h,84h,0FFh,0FEh,08h,80h,08h,90h
db 0Ah,90h,0Ch,60h,18h,40h,68h,0A0h
db 09h,20h,0Ah,14h,28h,14h,10h,0Ch
data ends
code segment
assume cs:code, ds:data
main:
mov ax, data
mov ds, ax
mov ax, 0A000h
mov es, ax
mov di, 0
mov ax, 0013h
int 10h
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; 刚移出的位会自动进入CF(进位标志)
jnc no_dot; 若没有进位即CF=0则跳到no_dot
is_dot:
mov byte ptr es:[di], 0Ch
no_dot:
add di, 1
sub cx, 1
jnz check_next_dot
sub di, 16
add di, 320
sub dx, 1
jnz next_row
mov ah, 1
int 21h
mov ax, 0003h
int 10h
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
KeyDown=1Eh KeyUp=9Eh # 按下和抬起 A
KeyDown=1Dh KeyUp=9Dh # left ctrl
KeyExtend=E0h KeyDown=1Dh KeyExtend=E0h KeyUp=9Dh # right ctrl
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
可以读取所有按键的编码 端口操作