OS_lab2

OS_Lab2

实验概述

在本次实验中,同学们会学习到x86汇编、计算机的启动过程、IA-32处理器架构和字符显存原理。根据所学的知识,同学们能自己编写程序,并且让计算机在启动后加载运行,增进对计算机启动过程的理解,为后面编写操作系统加载程序奠定基础。同时,同学们将学习使用gdb来调试程序的基本方法。

实验要求

DDL:2025.3.23

提交内容:将3+1(选做)个任务的代码实验报告放到压缩包中,命名为“lab2-姓名-学号”,提交到实验课程邮箱:os_sysu_lab@163.com

将实验报告的pdf提交至 http://inbox.weiyun.com/zPIW1se1

实验入门

  • 从汇编语言开始–汇编语言提供了一些特权指令,而高级指令并未提供对应的指令。

体系结构

  • IA-32处理器:从Intel 80386开始到32位的奔腾4处理器

  • Intel 32位的处理器也被称为x86处理器

  • IA-32处理器有三种基本操作模式:保护模式*、*实地址模式(简称实模式)和系统管理模式

  • IA-32的重要组成部分:

    • 地址空间:保护模式:使用32位地址总线、32位寄存器;实模式:20位地址总线、16位寄存器
    • 基本寄存器:IA-32处理器主要有8个通用寄存器eax, ebx, ecx, edx, ebp, esp, esi, edi、6个段寄存器cs, ss, ds, es, fs, gs、标志寄存器eflags、指令地址寄存器eip。
    • 通用寄存器:通用寄存器有8个,分别是eax, ebx, ecx, edx, ebp, esp, esi, edi,均是32位寄存器
      • 通用寄存器用于算术运算和数据传输。32位寄存器用于保护模式,为了兼容16位的实模式,每一个32位寄存器又可以拆分成16位寄存器和8位寄存器来访问。
0-31位 0-15位 8-15位 0-7位
eax ax ah al
ebx bx bh bl
ecx cx ch cl
edx dx dh dl

esi,edi,ebp和esp并无8位的寄存器访问方式

0-31位 0-15位
esi si
edi di
esp sp
ebp bp

通用寄存器的特殊用法:

  • eax乘法和除法指令中被自动使用,通常称之为扩展累加寄存器
  • ecx在loop指令中默认为循环计数器。

  • esp用于堆栈寻址。因此,我们绝对不可以随意使用esp。

  • esi和edi通常用于内存数据的高速传送,通常称之为扩展源指针和扩展目的指针寄存器。

  • ebp通常出现在高级语言翻译成的汇编代码中,用来引用函数参数和局部变量。除非用于高级语言的设计技巧中,ebp不应该在算术运算和数据传送中使用。ebp一般称之为扩展帧指针寄存器。

  • 段寄存器。段寄存器有cs, ss, ds, es, fs, gs,用于存放段的基地址,段实际上就是一块连续的内存区域。

  • 指令指针eip存放下一条指令的地址。有些机器指令可以改变eip的地址,导致程序向新的地址进行转移,如ret指令。

  • 状态寄存器。eflags存放CPU的一些状态标志位。下面提到的标志如进位标志实际上是eflags的某一个位。常用的标志位如下。

    • 进位标志(CF)。在无符号算术运算的结果无法容纳于目的操作数时被置1。
    • 溢出标志(OF)。在有符号算术运算的结果无法容纳于目的操作数时被置1。
    • 符号标志(SF)。在算术或逻辑运算产生的结果为负时被置1。
    • 零标志(ZF)。在算术或逻辑运算产生的结果为0时被置1。

实地址模式

  • 如上述所说,实地址模式的寄存器都是16位的,因此名称都不带e
  • 实地址的地址线是20位的,但寄存器都是16位的–采用“段地址+偏移地址

$$
物理地址=(段地址<<4)+偏移地址
$$

段寄存器也有约定俗成的规则。一个典型的程序有3个段,数据段、代码段和堆栈段。

  • cs包含16位代码段的基地址。
  • ds包含16位数据段的基地址。
  • ss包含一个16位堆栈段的基地址。
  • es、fs和gs可以指向其他数据段的基地址。

由于段地址必须通过段寄存器给出,因此下面直接用“段寄存器”来代替“段地址”,即物理地址可表示为“段寄存器:偏移地址”。

汇编基础

  • 汇编代码一般保存在以 .s.asm 为后缀的文件中。(我们可以在终端采用特定命令将高级语言代码转换为汇编代码)
寄存器 作用
ax 累加寄存器
cx 计数寄存器
dx 数据寄存器
ds 数据段寄存器
es 附加段寄存器
bx 基地址寄存器
si 源变址寄存器
di 目的变址寄存器
cs 代码段寄存器
ip 指令指针寄存器
ss 栈段寄存器
sp 栈指针寄存器
bp 基指针寄存器
flags 标志寄存器
  • 汇编注释:在汇编代码中使用分号 ;来注释
  • 在汇编代码中,一行只能写一条汇编语句无需以任何符号结尾
    • 例子:add eax,3 ;这是注释

nasm汇编–标识符

  • 标识符用来表示变量、常量、过程或代码标号
    • 标识符包含1-247个字符
    • 对大小写不敏感!
    • 标识符第一个字符必须是字母、下划线或@;不可以是数字
    • 标识符不能与汇编器的保留字相同。

nasm汇编–标号

  • 标号是充当指令或数据位置标记的标识符;标号的值就是其后指令或数据的起始地址(偏移地址)
  • 数据标号标识了变量的地址,为在代码中引用该变量提供了方便。
数据类型 含义
db 一个字节
dw 一个字,2个字节
dd 双字,4个字节

例子:

1
2
3
4
array dw 1024, 2048
dw 4096, 8192

array dw 1024, 2048, 4096, 8192 ;和上面是同样的

对应了:

array[0] = 1024
array[1] = 2048
array[2] = 4096
array[3] = 8192

  • 代码标号:代码标号标识了汇编指令的起始地址,通常作为跳转指令的操作数
  • 代码标号后面必须要有冒号 : ;但数据标号后面没有

数据传送指令

符号 含义
<reg> 寄存器,如ax,bx等
<mem> 内存地址,如标号var1,var2等
<con> 立即数,如3,9等
  • mov指令:将源操作数的内容复制到目的操作数
1
2
3
4
5
6
mov <reg>, <reg>
mov <reg>, <reg>
mov <reg>, <mem>
mov <mem>, <reg>
mov <reg>, <con>
mov <mem>, <con>

Intel汇编中,前者是目的操作数,后者是源操作数

nasm汇编–内存寻址方法

  • 寄存器寻址:mov ax,cx

  • 立即数寻址:mov ax,7 or mov ax,tag (tag表示的是标号)

  • 直接寻址:mov ax, [0x5c00] ; ax = 0xFF

    • 在根据偏移地址去取内存中的变量时,要加上 [],否则就只是将变量地址放到寄存器中

      mov ax, [tag] ; ax = 0xFF

    • 我们指令中如果没有显式指定段地址,那么我们的地址就是偏移地址

    • 访问数据段,使用段寄存器ds。

    • 访问代码段,使用段寄存器cs。

    • 访问栈段,使用段寄存器ss。
  • 基址寻址:基址寻址使用基址寄存器和立即数来构成真实的偏移地址

    • 基址寄存器只能是bx或bp;用bx做基址寄存器时,段地址寄存器默认为ds,使用bp时默认为ss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
; 使用bx做基址寄存器时段寄存器为ds存放的内容
mov [bx], ax
mov ax, [bx]
mov [bx + 3], ax
mov ax, [bx + 3]
mov [bx + 3 * 4], ax
mov ax, [bx + 3 * 4]
; 使用bp做基址寄存器时段寄存器为ss存放的内容
mov [bp], ax
mov ax, [bp]
mov [bp + 3], ax
mov ax, [bp + 3]
mov [bp + 3 * 4], ax
mov ax, [bp + 3 * 4]
  • 变址寻址:变址寻址使用变址寄存器和立即数来构成真实的偏移地址。

    • 变址寄存器只能是 sidi,默认段寄存器为 ds

      1
      2
      mov ax, [si + 4 * 4]
      mov [di], 0x5
  • 基址变址寻址:我们通过基址寄存器、变址寄存器、立即数来构成真实的偏移地址。默认段地址由基址寄存器的类型确定,即 bx对应 dsbp对应 ss,如下所示。

    1
    2
    3
    4
    5
    mov [bx + si + 5 * 4], ax
    mov [bx + di + 5 * 4], ax
    mov ax, [bx + si + 5 * 4]
    mov ax, [bp + si + 5 * 4]
    mov ax, [bp + di + 5 * 4]

x86汇编–算数和逻辑指令

  1. add指令:前面是目的操作数,所以const不可以在前面。

    1
    2
    3
    4
    5
    6
    7
    8
    add <reg>, <reg>
    add <reg>, <mem>
    add <mem>, <reg>
    add <reg>, <con>
    add <mem>, <con>
    ; e.g.
    add ax, 10 ; eax := eax + 10
    add byte[tag], al
  2. sub指令:类似于add

    1
    2
    3
    4
    5
    6
    7
    8
    sub <reg>, <reg>
    sub <reg>, <mem>
    sub <mem>, <reg>
    sub <reg>, <con>
    sub <mem>, <con>
    ; e.g.
    sub al, ah ; al := al - ah
    sub ax, 126
  3. imul是整数相乘指令,它有两种指令格式,一种为两个操作数,将两个操作数的值相乘,并将结果保存在第一个操作数中,第一个操作数必须为寄存器;第二种格式为三个操作数,其语义为:将第二个和第三个操作数相乘,并将结果保存在第一个操作数中,第一个操作数必须为寄存器

1
2
3
4
5
6
7
imul <reg>, <reg>
imul <reg>, <mem>
imul <reg>, <reg>, <con>
imul <reg>, <mem>, <con>
; e.g.
imul eax, [var] ; eax = eax * [var]
imul esi, edi, 25 ; esi = edi * 25
  1. idiv完成整数除法操作,idiv只有一个操作数,此操作数为除数,而被除数则为 edx:eax中的内容(一个64位的整数),操作的结果有两部分:商和余数,其中放在eax寄存器中,而余数则放在edx寄存器

    1
    2
    3
    4
    5
    idiv <reg> ;这里给出的是除数
    idiv <mem>
    ; e.g.
    idiv ebx
    idiv dword[var]
  2. inc,dec指令分别表示自增1或自减1

    1
    2
    3
    4
    5
    6
    7
    inc <reg>
    inc <mem>
    dec <reg>
    dec <mem>
    ; e.g.
    dec ax
    inc byte[tag]
  3. and, or, xor分别表示将两个操作数逻辑与、逻辑或和逻辑异或后放入到第一个操作数中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    and <reg>, <reg>
    and <reg>, <mem>
    and <mem>, <reg>
    and <reg>, <con>
    and <mem>, <con>

    or <reg>, <reg>
    or <reg>, <mem>
    or <mem>, <reg>
    or <reg>, <con>
    or <mem>, <con>

    xor <reg>, <reg>
    xor <reg>, <mem>
    xor <mem>, <reg>
    xor <reg>, <con>
    xor <mem>, <con>
  4. not表示对操作数每一位取反

1
2
3
4
5
6
7
not <reg>
not <mem>
; e.g.
not ax
not word[tag] ; 取反一个字,2个字节
not byte[tag] ; 取反一个字节
not dword[tag] ; 取反一个双字,4个字节
  1. neg表示取负
1
2
neg <reg>
neg <mem>
  1. shl,shr表示逻辑左移和逻辑右移
1
2
3
4
5
6
7
8
9
10
11
; cl是寄存器ecx的低8位寄存器

shl <reg>, <con>
shl <mem>, <con>
shl <reg>, cl
shl <mem>, cl

shr <reg>, <con>
shr <mem>, <con>
shr <reg>, cl
shr <mem>, cl

控制转移指令

  • jmp无条件跳转–jmp <label>

  • jcondition有条件跳转

    1
    2
    3
    4
    5
    6
    7
    je <label>   ; jump when equal
    jne <label> ; jump when not equal
    jz <label> ; jump when last result was zero
    jg <label> ; jump when greater than
    jge <label> ; jump when greater than or equal to
    jl <label> ; jump when less than
    jle <label> ; jump when less than or equal to
  • cmp指令:操作数1-操作数2,结果与机器状态寄存器eflags中的条件码比较

栈操作指令

  • 栈的增长方式是从高地址向低地址增长
  • push指令:将操作数压入内存的栈中;可以对reg,mem,con做
  • pop指令:将栈顶的数据放入到操作数中;reg,mem
  • pushad指令是将ax, cx, dx, bx, sp, bp, si, di 依次压入栈中。
  • popad指令是对栈指令一系列的pop操作,pop出的数据放入到di, si, bp, sp, bx, dx, cx, ax中。

过程调用

call和ret指令是用来实现子过程(或者称函数,过程,意思相同)调用和返回。call指令首先将当前eip的内容入栈,然后将操作数的内容放入到eip中。ret指令将栈顶的内容弹出栈,放入到eip中。

计算机开机启动过程

  • 计算机的启动需要程序加载,而计算机不启动则无法运行程序。
  • 加电开机–BIOS启动–加载MBR–硬盘启动–内核启动

Example1:Hello World

  • task:在MBR被加载到内存地址0x7c00之后,向屏幕输出蓝色的Hello World
  • 为了便于控制显示,IA-32处理器将显示矩阵映射到了内存0xB8000~0xBFFFF处,称为显存地址。
  • 在文本模式下,控制器的最小可控制单位为字符,每一个显示字符自上向下,从左到右依次使用显存地址中的两个字节表示–低字节表示所要显示的字符,高字节表示字符的颜色属性

显存对应关系

字符的颜色属性的字节高四位为背景色,低四位为前景色,具体如下:

字符属性对应表

  • 我们使用的是二维的点,但是在栈上是线性的 =》转换为一维的点

    $$
    \text{显存起始位置}=\text{0xB8000}+2\cdot(80\cdot x+y)
    $$

    (x,y)=(row,col),公式中的乘2是因为每个显示字符会使用两个字节表示

编写MBR:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
org 0x7c00 ; MBR被加载到内存地址0x7c00
[bits 16] ; 指定代码在16位实模式下运行
xor ax, ax ; eax = 0 相当于mov ax,0,但是前者机器码更短,执行更快
; 初始化段寄存器, 段地址全部设为0
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax

; 初始化栈指针
mov sp, 0x7c00
mov ax, 0xb800 ;0xb800是显存的段地址
mov gs, ax

;ah高位,al低位
mov ah, 0x01 ;蓝色--背景色是黑色,字是蓝色--0000 0001=0x01
mov al, 'H'
mov [gs:2 * 0], ax

mov al, 'e'
mov [gs:2 * 1], ax

mov al, 'l'
mov [gs:2 * 2], ax

mov al, 'l'
mov [gs:2 * 3], ax

mov al, 'o'
mov [gs:2 * 4], ax

mov al, ' '
mov [gs:2 * 5], ax

mov al, 'W'
mov [gs:2 * 6], ax

mov al, 'o'
mov [gs:2 * 7], ax

mov al, 'r'
mov [gs:2 * 8], ax

mov al, 'l'
mov [gs:2 * 9], ax

mov al, 'd'
mov [gs:2 * 10], ax

jmp $ ; 死循环

times 510 - ($ - $$) db 0
db 0x55, 0xaa

org是汇编语言中的伪指令,用于指令程序 <u>加载到内存中的起始地址 </u>

0x7c00 是 BIOS 将引导扇区加载到内存中的固定地址。BIOS 在启动时会自动将磁盘的第一个扇区(512字节)加载到内存地址 0x7c00,然后从这里开始执行。

6个段寄存器cs, ss, ds, es, fs, gs

由于汇编不允许使用立即数直接对段寄存器赋值,所以要借助ax,在第三行给ax赋值为0之后,再让ax给段寄存器赋值

$表示当前汇编地址,$$表示代码开始的汇编地址。times 510 - ($ - $$) db 0表示填充字符0直到第510个字节

  • 利用nasm汇编器来将代码编译成二进制文件:

    nasm -f bin mbr.asm -o mbr.bin

    • -f参数制定了输出文件格式
    • -o指定的是输出的文件名

    image-20250305172419639

  • 生成MBR后,将其写入到硬盘的首扇区。

    • 创建一个虚拟磁盘– qemu-img create filename [size]

      • qemu-img create hd.img 10m
    • 将MBR写入 hd.img的首扇区–使用 dd命令
      dd if=mbr.bin of=hd.img bs=512 count=1 seek=0 conv=notrunc

      • if表示输入文件。
      • of表示输出文件。
      • bs表示块大小,以字节表示。
      • count表示写入的块数目。
      • seek表示越过输出文件中多少块之后再写入。
      • conv=notrunc表示不截断输出文件,如果不加上这个参数,那么硬盘在写入后多余部份会被截断。
    • 启动qemu来模拟计算机启动:

      qemu-system-i386 -hda hd.img -serial null -parallel stdio

      • -hda hd.img表示将文件 hd.img作为第0号磁盘映像
      • -serial dev表示重定向虚拟串口到空设备中。
      • -parallel stdio 表示重定向虚拟并口到主机标准输入输出设备中。
      • GDB是什么? - C语言中文网 更多参数详见文档

9cbfaf1fe8a7437aae3bc3d5cfd9aa9

debug

使用gdb来配合qemu来进行debug,需要在qemu的启动命令中加入 -s -S参数,举例:

1
qemu-system-i386 -hda hd.img -s -S -parallel stdio -serial null

在另一个终端进入gdb,让gdb连接上qemu:target remote:1234

GDB是什么? - C语言中文网

本次实验会用到的指令:

gdb指令 含义
b *address 在内存地址address中设置断点
r 运行程序
c 继续运行
p *addr 打印地址的值
info registers 查看寄存器
x/10i $pc 显示从程序计数器的地址开始的10条汇编指令
set disassembly-flavor intel 设置gdb反汇编的语法为intel风格

任务1 MBR

1.1 复现example1

done!9cbfaf1fe8a7437aae3bc3d5cfd9aa9

1.2 修改example1的代码

修改Example 1的代码,使得MBR被加载到0x7C00后在(12,12)处开始输出你的学号。注意,你的学号显示的前景色和背景色必须和教程中不同。说说你是怎么做的,并将结果截图。

编译汇编文件:nasm -f bin mbr.asm -o mbr2.bin

创建虚拟磁盘: qemu-img create hd.img 10m

将MBR写入hd.img的首扇区: dd if=mbr2.bin of=hd.img bs=512 count=1 seek=0 conv=notrunc

启动qemu:

1
qemu-system-i386 -hda hd.img -serial null -parallel stdio

结果:image-20250305180510397

任务2 实模式中断

资料:

INT10H中断服务详解-CSDN博客

AH 功 能 调用参数 返回参数 / 注释
1 置光标类型 (CH)0―3 = 光标开始行   (CL)0―3 = 光标结束行
2 置光标位置 BH = 页号   DH = 行   DL = 列
3 读光标位置 BH = 页号 CH = 光标开始行  CL = 光标结束行  DH = 行  DL = 列
4 读光笔位置 AH=0 光笔未触发 =1 光笔触发 CH=象素行 BX=象素列 DH=字符行 DL=字符列
5 显示页 AL = 显示页号
6 屏幕初始化或上卷 AL = 上卷行数   AL =0全屏幕为空白   BH = 卷入行属性   CH = 左上角行号   CL = 左上角列号   DH = 右下角行号   DL = 右下角列号
7 屏幕初始化或下卷 AL = 下卷行数   AL = 0全屏幕为空白   BH = 卷入行属性   CH = 左上角行号   CL = 左上角列号   DH = 右下角行号   DL = 右下角列号
8 读光标位置的属性和字符 BH = 显示页 AH = 属性  AL = 字符
9 在光标位置显示字符及其属性 BH = 显示页   AL = 字符   BL = 属性   CX = 字符重复次数
A 在光标位置只显示字符 BH = 显示页   AL = 字符   CX = 字符重复次数
E 显示字符(光标前移) AL = 字符   BL = 前景色 光标跟随字符移动
13 显示字符串 ES:BP = 串地址   CX = 串长度   DH, DL = 起始行列   BH = 页号   AL = 0,BL = 属性   串:Char,char,……,char   AL = 1,BL = 属性   串:Char,char,……,char   AL = 2   串:Char,attr,……,char,attr   AL = 3   串:Char,attr,……,char,attr 光标返回起始位置  光标跟随移动  光标返回起始位置  光标跟随串移动

应用举例:汇编语言——一些中断的调用 - b1ing丶 - 博客园

本任务会用到的:

功能 功能号 参数 返回值
设置光标位置 AH=02H BH=页码,DH=行,DL=列
获取光标位置和形状 AH=03H BX=页码 AX=0,CH=行扫描开始,CL=行扫描结束,DH=行,DL=列
在当前光标位置写字符和属性 AH=09H AL=字符,BH=页码,BL=颜色,CX=输出字符的个数

中断的调用方式:

1
2
3
将参数和功能号写入寄存器
int 中断号
从寄存器中取出返回值

2.1

请探索实模式下的光标中断int 10h实现将光标移动至(8,8),获取并输出光标的位置。说说你是怎么做的,并将结果截图。

做法:

  • 利用任务1中打印到输出端的方法,先将段寄存器初始化为0,以及栈顶指向显示矩阵的首地址。

  • 设置光标的位置:ah=2

    1
    2
    3
    4
    5
    mov ah,2    ;设置光标位置
    mov bh,0 ;page set to be 0
    mov dh,8 ;row
    mov dl,8 ;col
    int 10h
  • 获取光标的位置:ah=3

    1
    2
    3
    mov ah,3    ;获取光标位置和形状
    mov bx,0
    int 10h
  • 再将坐标字符送到显示矩阵中存储:要注意这里的需要将二进制转换为ASCII字符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    mov al, '('
    mov [gs:2*(80*8+8)],ax

    mov al,dh
    add al,'0' ;将二进制转换为ASCII字符
    mov [gs:2*(80*8+9)],ax

    mov al,','
    mov [gs:2*(80*8+10)],ax

    mov al,dl
    add al,'0'
    mov [gs:2*(80*8+11)],ax

    mov al,')'
    mov [gs:2*(80*8+12)],ax
  • ending

    1
    2
    3
    jmp $
    times 510-($-$$) db 0
    db 0x55,0xaa

结果:

2

2.2

利用实模式下的中断,从(8,8)开始输出你的学号。说说你是怎么做的,并将结果截图。

  • 光标移动到(8,8),复用2.1任务中的指令

  • 在光标位置输出学号,需要利用光标位置来计算显存矩阵位置的偏移量,然后循环打印学号即可,过程中要记得将地址+2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
;计算偏移量--输出移动到光标处--2*(80*x+y)
mov ax,80
mul dh
add al,dl
adc ah,0
shl ax,1
mov di,ax

;把学号存储在寄存器中
mov si,student_id
mov cx,8 ;长度
mov ah,0x0A


print_loop:
lodsb ;从si加载字符到al
mov ah,0x0A
mov [gs:di],ax
add di,2 ;累加,往后输出
loop print_loop

jmp $
student_id db '23336139'
times 510-($-$$) db 0
db 0x55,0xaa

光标移动到(8,8),并从此开始输出我的学号。

  • 结果:

image-20250306104000712

2.3

参考《汇编语言》第17章、键盘I/O中断调用。关于键盘扫描码,可以参考键盘扫描码表

在2.1和2.2的知识的基础上,探索实模式下的键盘中断int 16h利用键盘中断,实现任意 键盘输入并回显 的效果。说说你是怎么做的,并将结果截图。

INT 16H(键盘I/O中断)
  AH=0:从键盘读入ASCII字符,放在AL中。
  AH=1:测试有无键被按下。ZF=0,表示按过任意键,并在AL中获得该键的ASCII码。ZF=1,未按过键。
  AH=2:读取特殊功能键的状态至AL中。

D7 D6 D5 D4 D3 D2 D1 D0
Ins CapsLock NumLock ScrollLock Alt Ctrl 左Shift 右Shift

过程:

  • 为了在显存输出,也是类似地进行初始化。

  • 会使用到int 16h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    org 0x7c00      
    [bits 16]


    xor ax, ax ; 清零 ax
    mov ds, ax ; 初始化段寄存器
    mov es, ax
    mov ss, ax
    mov sp, 0x7c00 ; 设置栈指针

    mov ax, 0xb800 ; 设置显存段地址
    mov gs, ax

    mov ah, 0x00 ; 从键盘读取一个字符
    int 0x16 ; AL = ASCII 字符,AH = 扫描码

    mov ah, 0xF2 ; 设置字符属性--颜色
    mov [gs:0], ax ; 将字符和属性写入显存

    jmp $ ; 无限循环

    times 510-($-$$) db 0 ; 填充剩余空间
    db 0x55, 0xaa ; 引导扇区结束标志

结果:

image-20250306114833724

image-20250306114913976

任务3 汇编

要求:

  • 任务3使用的是32位寄存器
  • a1if_flagmy_random等都是预先定义好的变量和函数,直接使用即可。
  • 需要补全的代码文件在 assignment/student.asm中。
  • 代码编写好之后使用 make run 来测试代码
  • 你可以修改 test.cpp中的 student_setting中的语句来得到你想要的 a1,a2
  • 最后附上结果截图

3.1 分支逻辑的实现

请将下列伪代码转换成汇编代码,并放置在标号 your_if之后。

1
2
3
4
5
6
7
if a1 < 12 then
if_flag = a1 / 2 + 1
else if a1 < 24 then
if_flag = (24 - a1) * a1
else
if_flag = a1 << 4
end

条件分支

  • 利用跳转指令、跳跃指令、条件跳转指令
1
jmp label
  • 条件跳转指令:
1
2
cmp <源操作数>, <目标操作数>
jcc <目标地址>
  • 用于算术运算:
指令 描述 标志测试
JE/JZ 跳转等于或跳转零 ZF
JNE/JNZ 跳转不等于或跳转不为零 ZF
JG/JNLE 跳转大于或跳转不小于/等于 OF, SF, ZF
JGE/JNL 跳转大于/等于或不小于跳转 OF, SF
JL/JNGE 跳转小于或不大于/等于 OF, SF
JLE/JNG 跳少/等于或跳不大于 OF, SF, ZF
  • 逻辑运算:
指令 描述 标志测试
JE/JZ 跳转等于或跳转零 ZF
JNE/JNZ 跳转不等于或跳转不为零 ZF
JA/JNBE 跳转向上或不低于/等于 CF, ZF
JAE/JNB 高于/等于或不低于 CF
JB/JNAE 跳到以下或跳到不高于/等于 CF
JBE/JNA 跳到下面/等于或不跳到上方 AF, CF

结果代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
your_if:
mov eax,[a1] ;提取值
cmp eax,12
jl label1

cmp eax,24
jl label2
jmp label3

label1:
mov ebx,2
div ebx
inc eax
mov [if_flag],eax
jmp if_done


label2:
mov ebx,eax
mov eax,24
sub eax,ebx
imul ebx
mov [if_flag],eax
jmp if_done

label3:
shl eax,4
mov [if_flag],eax

if_done:

只要搞清楚如何分析分支跳转就可以了!

div: 只有一个操作数,计算后放在eax寄存器中,而余数则放在edx寄存器

3.2 循环逻辑的实现

请将下列伪代码转换成汇编代码,并放置在标号 your_while之后。

1
2
3
4
5
while a2 >= 12 then
call my_random // my_random将产生一个随机数放到eax中返回
while_flag[a2 - 12] = eax
--a2
end
  • ​ 无条件循环指令
1
jmp <目标地址>
  • 条件循环指令
1
2
cmp <源操作数>, <目标操作数>
jcc label

结果:(更新)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
your_while:
mov ebx, [a2] ; 读取 a2,这里不能用 eax 存储 [a2] 的值,调用 my_random 返回的随机数会覆盖 eax!!!

loop:
cmp ebx, 12
jl while_end ; 若 a2 < 12,则退出循环

call my_random ; 产生随机数,存入 eax
; 计算 while_flag[a2 - 12] 的地址
mov edx, ebx
sub edx, 12 ; edx = a2 - 12
mov ecx, [while_flag]
add edx, ecx ; edx = while_flag + (a2 - 12)
mov [edx], al ; 将随机数存入 while_flag[a2 - 12]
dec ebx
mov [a2], ebx
jmp loop

while_end:

重点:

  • 不可以把a2的值读取到eax中

  • 要记得把地址 *4 ,因为存储的是字节

3.3 函数的实现

请编写函数 your_function并调用之,函数的内容是遍历字符数组 string

1
2
3
4
5
6
7
8
9
10
your_function:
for i = 0; string[i] != '\0'; ++i then
pushad
push string[i] to stack
call print_a_char
pop stack
popad
end
return
end

结果:(更新–旧版本没意识到出错了……)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
your_function:
; put your implementation here
mov ecx,0 ;i设置为0
loop_start:
mov edx,[your_string]
mov al,[edx+ecx]
cmp al,0
je function_end

pushad ;保存所有通用寄存器的状态
push eax ;将字符压入栈中
call print_a_char
add esp,4 ;弹出
popad ;恢复状态

inc ecx
jmp loop_start

function_end:
ret

重点:

  • 在 32 位汇编中,栈的操作单位是 4 字节(32 位)。无论是压栈(push)还是弹栈(pop),都是以 4 字节为单位进行的。
  • 函数要记得返回

make run结果:

任务 4 汇编小程序(选做)

字符弹射程序。请编写一个字符弹射程序,其从点(2,0)处开始向右下角45度开始射出,遇到边界反弹,反弹后按45度角射出,方向视反弹位置而定。同时,你可以加入一些其他效果,如变色,双向射出等。注意,你的程序应该不超过510字节,否则无法放入MBR中被加载执行。静态示例效果如下,动态效果见视频assignment/assignment-4-example.mp4

实现思路:

  • 设置显示字符的行、列寄存器(ch-行,cl-列),然后设置两个寄存器分别用于做水平方向和竖直方向的位移(最初设置为1,1是因为我们要从(2,0)点出发往右下方45°移动

  • 设置一个专门进行弹跳的循环

    • 判断是否到达边界,如果到达边界需要反转对应方向的增量(其实还可以直接使用neg反转)
    • 边界调整完后进入展示循环
  • 展示循环

    • 需要计算当前字符需要被放置到的显存矩阵的位置(这里尤其要注意,我们原本放置的寄存器是8位寄存器,但是bx是16位寄存器,需要进行0扩展)
    • 将字符和颜色传入显存矩阵
    • 修改字符和颜色(简单地通过+1来实现)
    • 调用延时(由于我们把延时设置为一个标签了,这时候要保存原来的值后跳转出去需要借用到,在调用完之后返回再弹出栈)

实验代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
org 0x7c00      
[bits 16]
;基础设置
xor ax, ax ; 清零 ax
mov ds, ax ; 初始化段寄存器
mov es, ax
mov ss, ax
mov sp, 0x7c00 ; 设置栈指针

mov ax, 0xb800 ; 设置显存段地址
mov gs, ax

;初始化字符1:(2,0)--(ch,cl)=(row,col)
mov ch, 2 ; 初始行位置
mov cl, 0 ; 初始列位置
;增量,line1向右下移动(初始)
mov dh, 1 ; 行方向增量
mov dl, 1 ; 列方向增量

mov ah, 0x0A ; 字符颜色(亮绿色)
mov al, '0' ; 显示的字符

;显存矩阵大小:25*80
bounce_loop:
; 检查上下边界
cmp ch, 0
jle bounce_up ; 如果行 <= 0,需要向下反弹
cmp ch, 24
jge bounce_down ; 如果行 >= 24,需要向上反弹

; 检查左右边界
cmp cl, 0
jle bounce_right ; 如果列 <= 0,需要向右反弹
cmp cl, 79
jge bounce_left ; 如果列 >= 79,需要向左反弹

jmp display ; 通过所有边界检查,显示字符

bounce_up:
mov dh, 1 ; 将行方向改为向下(正)
jmp display

bounce_down:
mov dh, -1 ; 将行方向改为向上(负)
jmp display

bounce_right:
mov dl, 1 ; 将列方向改为向右(正)
jmp display

bounce_left:
mov dl, -1 ; 将列方向改为向左(负)
jmp display

display:
; 计算显存位置
movzx bx, ch ; 直接用ch*80会溢出 这里不能用mov--因为类型寄存器位数不同
imul bx, 80 ; bx *= 80
movzx si, cl
add bx, si ; bx = ch * 80 + cl
shl bx, 1 ; bx = 2 * (ch * 80 + cl)

; 当下的点传入显存矩阵
mov [gs:bx], al ; 写入字符
mov [gs:bx+1], ah ; 写入属性(颜色)

;增量
add ch, dh ; 更新行位置
add cl, dl ; 更新列位置
inc ah ; 改变颜色
inc al ; 改变字符

; 添加延迟--需要入栈存储寄存器的值,然后使用int 15h的86功能(延时),在时延后需要将寄存器的值弹出,同时返回继续显示
push ax
push cx
push dx

mov ah, 0x86
mov cx,0x0001
mov dx,0xa120
int 15h

delay:
loop delay
pop dx
pop cx
pop ax

jmp bounce_loop ; 继续主循环


jmp $ ; 死循环

times 510 - ($ - $$) db 0
db 0x55, 0xaa

本任务感谢隔壁舍友L同学的鼎力支持!从她的代码中学习到了不同位数寄存器要使用movzx存储,同时使用int 15h的0x86功能来产生延时 等知识。

此处同时贴上相关的知识点:

功能86H

BIOS设备中断 15号中断详解-CSDN博客

BIOS中断实现延时-CSDN博客

功能描述:延迟

入口参数:AH=86H

CX:DX=微秒

出口参数:CF=0——操作成功,AH=00H

  • 总微秒数 = CX * 65536 + DX
  • 例如,如果CX = 0001h,DX = 86A0h,则延时为: 0001h * 65536 + 86A0h = 65536 + 34464 = 100,000微秒 = 100毫秒
延时时间 CX值 DX值 16进制总微秒
10毫秒 0000h 2710h 0000’2710h
50毫秒 0000h C350h 0000’C350h
100毫秒 0001h 86A0h 0001’86A0h
250毫秒 0003h D090h 0003’D090h
500毫秒 0007h A120h 0007’A120h
1秒 000Fh 4240h 000F’4240h
5秒 004Ch 4B40h 004C’4B40h

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
; 延时100毫秒
delay_100ms:
mov ah, 86h
mov cx, 0001h ; 100,000微秒的高16位
mov dx, 86A0h ; 100,000微秒的低16位
int 15h
ret

; 延时1秒
delay_1s:
mov ah, 86h
mov cx, 000Fh ; 1,000,000微秒的高16位
mov dx, 4240h ; 1,000,000微秒的低16位
int 15h
ret

movzx

理解MOVZX指令:汇编语言数据传送的无符号扩展-CSDN博客

用处是将数据从源操作数复制到目的操作数,并使用零扩展将剩余高位填充为0.

movzx destination, source

代码中奖ch移动到bx,将一个8位的寄存器复制到16位寄存器并进行了零扩展。

类似:movsx(复制并进行符号扩展)


OS_lab2
https://pqcu77.github.io/2025/03/05/OS-lab2/
作者
linqt
发布于
2025年3月5日
许可协议