开机代码主要实现了两个事情:
(1)将操作系统从磁盘中读入内存当中;(2)使用操作系统进行初始化操作
计算机是怎么工作的?
最初,计算机起源于图灵机,是一种计算模型。图灵机最初的灵感来源是人在纸上计算模拟的过程,将这个过程用自动化的设备模拟出来就是图灵机。
首先在纸带上写上“3”、“2”、“+”,然后用控制区去模拟答案,用读写头去模拟眼睛和笔。
控制器首先将“3”、“2”和“+”读进来,然后使用已设置好的加法逻辑对读取的数据进行运算,得到最终结果,然后再写到纸带。
通用图灵机相对于图灵机来说增加了控制制器动作和控制器状态,可进行多种不同的操作,而不只是单一的操作。
将程序载入内存中,用一个IP(Instruction Pointer)也称PC(Program Counter)指针来指向存储器,将当前IP指针指向位置的对应指令加载进来,控制指令进行执行,实际上实现了通用图灵机的控制逻辑。
计算机工作的核心: 取值、执行
对于x86计算机,有一部分程序内容是固化的(ROM只读存储器中的内容),存储在0xFFFF0。
刚开机开机后执行步骤:
(1)x86计算机刚开机时,CPU处于实模式(地址直接对应到真实地址)。
(2)刚一上电计算机先自动设好CS=0xFFFF和IP=0x0000(代码段寄存器和端内偏移)
(3)找到0xFFFF0(ROM BIOS映射区)(跳转地址:CS左移4位,再加上IP:CS<<4 + IP),读取固化在ROM里的程序内容。
(4)程序实现检查内存(RAM)、键盘、显示器和软硬磁盘。
(5)BIOS再从磁盘0磁道0扇区读取内容,放到内存中0x7c00处(一个扇区512B,0磁道0扇区是操作系统的引导扇区,里面存储着操作系统的第一段代码)
(6)设置CS=0x07c0和IP=0X0000,然后BIOS将会跳转到引导扇区开始执行。
扩展资料:
CPU的实模式和保护模式(一)
实模式和保护模式
段寄存器
BIOS启动设备的第一个扇区将bootsect.s读入到0x7C00处。
boot sector(引导扇区),采用汇编语言编写的代码bootsect.s,实现从磁盘上将setup模块(第2个扇区到第4个扇区)读入内存0x90000处,并打印出开机logo,然后将操作系统的system模块读入到0x10000处。
mov ax, #BOOTSEG // 0x07c0
mov ds, ax
将0x07c0(存储在BOOTSEG)存储在ax(accumulator累加寄存器)中,然后再将ax中内容存储在ds(Data Segement数据段寄存器)中。(ds不能直接用mov指令赋值,需要通过寄存器到寄存器赋值,用来存放要访问数据的段地址)
mov ax, #INITSEG // 0x9000
mov es, ax
es(Extra Segement)的操作同理,将INITSEG(initialize segment)让es=0x9000。(附加段寄存器ES:存放当前执行程序中一个辅助数据段的段地址。 段寄存器 偏移地址寄存器)
ds寄存器:记录数据从哪里来,源数据地址;
es寄存器:记录数据到哪里去,目的地址。
sub si, si
sub di,di
一个地址要有段基址和端内偏移,SI源变址寄存器,用来存放相对于DS段之源变址指针;DI目地变址寄存器,用来存放相对于 ES 段之目的变址指针。
sub(subtract) 是实现第一个寄存器中的数减去第二个寄存器中的数,并将结果存放在第一个寄存器中。
上述语句执行后,得到si=0和di=0,与ds和es相接后,得到ds:si=0x7c000、es:di=0x90000。
rep movw
rep(repeat)指令作用是根据cx中的值,重复后面的指令。
movw(move word)指令用于对16位字(1个word)的值进行mov操作,将DS:SI中的内容送至ES:DI中。
上述语句执行后,实现重复移动字256个字(512个字节,1 word = 2 Byte = 16位),将0x07c00处的256个字移动到0x90000处,来腾出DS:SI中空间。
jmpi go, INITSEG // cs=INITSEG, ip=go
go:
mov ax,cs // cs=0x9000
mov ds,ax
mov es,ax // 段基址 0x9000
mov ss,ax // 堆栈段寄存器,存放段地址
mov sp,#0xff00 // 堆栈指针寄存器,存放偏移地址
因bootsect已被挪动,所以需要写jmpi来指明程序后续执行地址进行执行。
jmpi(jump indirection)指令实现间接跳转,将会修改IP和CS中的值。实现将go(后面定义的一个标号)赋给IP,INITSEG( 0x9000)赋给CS(代码段寄存器,一般用于存放代码),从而跳转到 INITSEG:go (0x9000 >> 4 + go = 0x90000 + go)处执行。
将ds、es和ss都设置为0x9000,将堆栈指针sp指向0x9ff00
ss 为栈段寄存器,后面要配合栈基址寄存器 sp 来表示此时的栈顶地址。而此时 sp 寄存器被赋值为了 0xff00 了,所以目前的栈顶地址就是 ss:sp 所指向的地址 0x9ff00 处。
栈是向下发展的,这个栈顶地址 0x9ff00 要远远大于此时代码所在的位置 0x90000,所以栈向下发展就很难撞见代码所在的位置,也就比较安全。这也是为什么给栈顶地址设置为这个值的原因,其实只需要离代码的位置远远的即可。
load_setup:
mov dx, #0x0000 // 数据寄存器
mov cx, #0x0002 // 计数寄存器
mov bx, #0x0200 // 基址寄存器
mov ax, #0x0200+SETUPLEN // 累加器
int 0x13 // BIOS中断
jnc ok_load_setup // 跳转指令, CF=0则跳转。如果没有读错,则跳转到加载setup执行;如果读错,则复位驱动器重新读。
mov dx, #0x0000 // 对驱动器0进行操作
mov ax, # 0x0000 // 复位
int 0x13 // 终端指令,将在指定扇区中的代码
j load_setup // 重读
int指令用于向CPU发送中断,后跟中断码的类型。int 0x13中断向量所指向的中断服务程序实质上就是磁盘服务程序,用于将指定扇区的代码加载到内存的指定位置。
实现从第二个扇区开始,往后读4个扇区的内容(setup的4个扇区)。从0x90000读到0x90200(一个扇区512B,es:bx=0x90200)
入口参数:
(1)ax(累加器,在乘、除法等操作中有专门的用途)由ah(高八位)和al(低八位)组成
(2)cx(计数寄存器,在循环操作时作计数器用,用于控制循环程序的执行次数)由ch(高八位)和cl(低八位)组成
(3)dx(数据寄存器,在乘、除法及I/O端口操作时有专门用途)由dh(高八位)和dl(低八位)组成
(4)es:bx合成一个目标内存地址,决定了要读到哪里。
然后,再把system模块读入到0x10000处。
总结
start:
// 移动bootsect,将0x07c0:0x0000处的256个字移动到0x90000:0x0000处
mov ax, #BOOTSEG mov ds, ax // 0x07C0
mov ax, #INITSEG mov ex, ax // 0x9000
mov cx, #256
sub si, si sub di,di
rep movw // 将ds:si往后512B的内容复制到es:di处,将bootsect移动到了0x90000,腾空位置。
// 跳转到0x9000:go处,将设置es、ss、sp都为0x9000(相当于是初始化)
jmpi go, INITSEG // 0x9000
go: mov ax, cs // 0x9000
// 将段寄存器设置为0x9000、栈顶地址设置为0x9fff00,来远离代码位置
mov ds, ax mov ex, ax mov ss, ax mov sp, #0xff00
load_setup: // 载入setup模块
// 将指定的扇区内容(setup模块)读到内存中0x90200处
mov dx, #0x0000 // 数据寄存器
mov cx, #0x0002 // 计数寄存器
mov bx, #0x0200 // 基址寄存器
mov ax, #0x0200+SETUPLEN // 累加器
int 0x13 // BIOS读磁盘扇区中断
jnc ok_load_setup // 跳转指令, CF=0则跳转。如果没有读错,则跳转到加载setup执行;如果读错,则复位驱动器重新读。
mov dx, #0x0000 // 对驱动器0进行操作
mov ax, # 0x0000 // 复位
int 0x13 // 执行0x13中断(BIOS读磁盘扇区的中断)
j load_setup //重读
// 显示开机时的logo
ok_load_setup: // 载入setup模块,显示开机Logo,读取system模块
// 读取setup模块,显示开机logo
mov dl, #0x00 mov ax, #0x0800 // ah=8,获得磁盘驱动器的参数
int 0x13 mov ch, #0x00 mov sectors, cx
mov ah, #0x03 xor bh, bh int 0x00 // 读光标
mov cx, #24 mov bx, #0x0007 // 7是显示属性
mov bp, #msg1 mov ax, #1301 int 0x10 //显示字符
// 读取system模块到0x10000处
mov ax, #SYSSEG // SYSSEG=0X1000
mov es, ax
call read_it // 读入system模块(OS后续代码)
jmpi 0, SETUPSEG // 转入0x9020:0X0000执行setup.s
bootsect.s中的数据(在文件末尾)
sectors: .word 0 // 磁道扇区数
msg1: .byte 13, 10
.ascii "Loading system..." // 屏幕上显示的字
.byte 13, 10, 13, 10
拓展资料:
汇编指令汇总
(深入理解计算机系统) bss段,data段、text段、堆(heap)和栈(stack)
80x86 CPU 中寄存器
你管这叫操作系统源码(一)
setup模块,即setup.s,利用ROM BIOS中断读取机器系统数据,并将这些数据保存到0x90000开始的位置。然后,将system模块从0x10000-0x8ffff整块向下移动到内存绝对地址0x00000处。
start: mov ax, #INITSEG
mov ds, ax
将ds设置为INITSEG(0x9000),在setup程序中需要重新设置ds。
mov ah, #0x03
xor bh, bh
int 0x10
mov [0], dx
将光标位置信息读取到0x90000处,控制台初始化时会来取
0x10中断功能号ah=0x03,读取光标位置。
输入bh=页号,将其读到dx中(dh=行号(0x00顶端)、dl=列号(0x00最左边)),cx为扫描线(ch=扫描开始线、cl=扫描结束线)。
mov [0], dx:间接寻址,数据段ds(0x9000),偏移是0,对应内存的绝对地址是0x90000。实现了把dx中的值保存到内存地址0x90000处。
mov ah, #0x88
int 0x15
mov [2], ax
因操作系统需要管理内存,所以首先需要获取内存的大小。然后,需要知道内存的分配情况(运行的功能、分布的位置、大小等)。
int 0x15:获取物理内存的大小,ah=#0x88作为其参数(15号中断的88号子程序)。将获取到的值放到ax中,然后赋给 [2] (间接寻址,前面默认有个段寄存器,而所有的段寄存器已都指向了ds=0x9000)= 0x9000 << 4 + 2 = 0x90002,此处存储的是扩展内存数。
后面就是获取其他硬件信息,此处省略…
cli
mov ax, #0x0000
cld
cli指令:ClearInterrupt,禁止中断、关中断。让中断标志为0,IF=0。为1时是开中断对应STI(Set Interrupt)指令。
将ax设置为0x0000为后续将system模块移到0x00000做准备。
cld指令:使操作方向标志位DF(Direction Falg)置为0。当该位置1时(DF=1),每次操作后si,di递减。存储器地址自动减少,串操作指令为自动减量指令,即从高位到低位处理字符串;当该位置0时(DF=0),每次操作后si,di递增。存储器地址自动增加,串操作指令为自动增量指令。
为从0x00000处递增填充操作系统内容做准备。
do_move: mov es, ax // es=0x0000
add ax, #0x1000
将ax(0x0000)赋给es
add指令:实现两数相加,并将结果赋予第一个参数。
cmp ax, #0x9000
jz end_mov
实现当ax=0x9000时,结束移动(之前假设system模块最大长度不超过0x80000(512k),也就是末端不超过地址0x90000)。使用cmp判断ax和0x9000是否相同,即确定是否移动完成,若移动完成,则用jz进行跳转。
cmp(compare)指令,属于算术运算指令,它是从目的操作数中减去源操作数,并不修改任何操作数。
ZF(Zero Flag)零标志,运算结果为0时置为1,否则置为0。
CF(Carry Flag)进位标志,进位时置1,否则置0。
SF(Sgin Flag)符号标志,结果为负时置1,否则置0。
OF(Overflow Flag)溢出标志,溢出为1,否则置0。
jz:跳转指令,通过ZF标志位是否跳转,当执行到JZ或者JE指令时,如果ZF=1则跳转,如果ZF=0,不跳转。
mov ds, ax // ds=0x1000
sub di, di // di=0x0000,es:di=0x00000
sub si, si // si=0x0000,ds:si=0x10000
mov cx, #0x8000 // 一次移动一个字,移动了0x8000个字
rep movsw
jmp do_move
将处于0x10000开始处的system模块移动到0x00000位置,将从0x10000到0x8ffff的内存数据块(512KB)整块的向内存低处移动了0x10000(64KB)的位置。
ds被赋值为0x9000,将di和si清零。此时,ds:si=0x90000、es:ei=0x00000。
cx(Count):计数器寄存器,设置为0x8000,意为移动0x8000字(64KB字节)。
使用rep movsw操作,以字(word)为单位,将0x10000往后的代码(整个操作系统)都移动到0地址。
总结
// 获取光标位置、获取内存大小信息
start : mov ax, #INITSEG mov ds, ax mov ah, #0x03
xor bh, bh int 0x10 mov [0], dx // 将光标位置信息读取到0x90000处,控制台初始化时会来取
// 获取内存大小信息、物理设备等信息
mov ah, #0x88 int 0x15 mov [2], ax ...
cli // 关中断
mov ax, #0x0000, cld // 设置读取目标位置和递增读取方式
// 将system模块从0x10000动到0x00000处
do_mov: mov es, ax add ax, #0x1000
cmp ax, #0x9000 jz end_mov // 若ax=0x9000,表示移动完成,则跳出
mov ds, ax sub di, di sub si, si // 设置源地址和目的地址
mov cx, #0x8000 // 设置移动数据的长度,64KB=0x10000=0x8000字
rep movsw // 以word为单位进行移动
jmp do_move
拓展资料:
Linux内核剖析之setup.s简介
在这里开始,寻址方式将不再是实模式,而是保护模式。在实模式下,cs是16位,ip是16位通过左移4位加上ip的方法,也只有20位,相当于1M的空间,肯定是不够的,现在的内存是4G的,所以不能使用实模式寻址。
要从16位机(左移4位变成20位),切换到32位机的样子,即1M -> 4G。16位机和20位机的本质区别是CPU的寻址方式不一样。所以,我们要切换CPU的寻址方式。
对于16位的环境,只能寻址64KB的地址空间,所以通过地址线将其寻址方式扩展到20位,即1M,那么在实际的寻址中,实际地址=基地址*16+偏移地址
。
到了32位的时候,寻址方式就不同了,在setup.s这段代码中,还设置了GDT(全局描述表)
和IDT,(中段描述表)
分别用于保护模式下的 地址翻译
和中断处理
。
那么在进入保护模式下,对于setup.s最后一句,jmpi 0,8
,实现了让CS=8,IP=0,CS的值就是用来查找GDT表项的,最终不会跳到0x0008:0000处,而是会跳转到0x0000处,也就是system模块加载的起始地址,后面就开始system模块的执行了。
通过段描述符索引,可以从全局描述符表 gdt 中找到一个段描述符,段描述符里存储着段基址。段基址取出来,再和偏移地址相加,就得到了物理地址。
段寄存器(比如 ds、ss、cs)里存储的是段选择子,段选择子去全局描述符表中寻找段描述符,从中取出段基址。
end_move: mov ax, #SETUPSEG mov ds, ax
// 设置保护模式下的中断和寻址
lidi idt_48 // 加载中段描述符(idt)寄存器,idt_48是6字节操作数的位置。前2字节表示idt表的限长,后4字节表示idt表所处的基地址。
lgdt gdt_48 // 加载全局描述符(gdt)寄存器,gdt_48是6字节操作数的位置。
进入保护模式的命令...
idt_48: .word 0 .word 0, 0 // 保护模式的中断函数表
gdt_48: .word 0x800 .word 512+gdt, 0x9
// 初始化gdt
gdt: .word 0, 0, 0, 0 // 描述符表由多个8字节长的描述符组成。每一个word是16位,所以每一个表项是64位。
.word 0x07FF, 0x0000, 0x9A00, 0x00C0
.word 0x07FF, 0x0000, 0x9200, 0x00C0
lidt
指令用于加载中段描述表(idt)寄存器:
它的操作数是6个字节,0-1字节是描述符表的长度值(字节);2-5字节是描述符表的32位线性基地址(首地址)。中段描述符表中的每个表项(8B)指出发生中段时需要调用的代码信息,与中段向量有些相似,但要包含更多的信息。
lgdt
指令用于加载全局描述符表(gdt)寄存器:
操作数格式与lidt指令相同,其每个描述符表中的每个描述符项(8字节)描述了保护模式下数据和代码段(块)的信息。其中包括段的最大长度限制(16位)、段的线性基址(32位)、段的特权级、段是否在内存、读写许可以及其它一些保护模式运行的标志。
在实模式下,CS:IP翻译出来是CS << 4 + IP;
在保护模式下,CS:IP翻译出来是CS查表 + IP。
IDT通过int n
(中断系统调用号)来查找对应的中断处理函数。
逻辑地址由16位段的选择符和32位的偏移量组成。
段描述符表示段描述符的一个数组。描述符表的长度可变,最多可包含8192个8字节描述符。有两种描述符表:全局描述符GDT(Global descriptor table)和局部描述符表LDT(Local descriptor table)。
gdt(Global Descriptor Table,全局描述符表)完全用硬件实现,原因是硬件实现起来速度更快。
在保护模式下,根据cs在GDT表中的查找的值+ip来进行地址翻译。现在的cs被称为选择表中的选择子,以前的cs里放的就是地址,而现在cs里存放的是查表表项的索引。
为了有助于处理异常和中断,每个需要被处理器进行特殊处理的处理器定义的异常和中断条件都被赋予了一个标识号,称为向量(vector)。处理器把赋予异常或中断的向量用作中断描述符表IDT(Interrupt Descriptor Table)中的一个索引号,来定位一个异常或中断的处理程序入口点位置。
允许的向量好范围是0 ~ 255,其中0 ~ 31保留用作80X86处理器定义的异常和中断,不过目前该范围的向量号并非每个都已定义了功能,未定义功能的向量号将留作今后使用。
范围在32 ~ 255的向量号用于用户定义的中断。这些中断通常用于外部I/O设备,使得这些设备可以通过外部硬件中断机制向处理器发送中断。
中断源:
中断描述符IDT(Interrupt Descriptor Table)将每个异常或中断向量分别与它们的处理过程联系起来。与GDT和LDT表类似,IDT也是由8字节长描述符组成的一个数组。
与GDT不同的是,表中第1个项可以包含描述符。为了构成IDT表中的一个索引值,处理器把异常或中断的向量号*8。因为最多只有256个中断或异常向量,所以IDT无需包含多余256个描述符。IDT中可以含有少于256个描述符,因为只有可能发生的异常或中断才需要描述符。不过IDT中所有空描述符项应该设置其存在位(标志)为0。
IDT表32位的基址和16位的长度值。IDT表基址应该对其在8字节边界上以提高处理器的访问效率。限长值是以字节为单位的IDT表的长度。
在上面已经设置完了保护模式,下面的执行就会在保护模式下进行,采用GDT实现地址查询。
cr0寄存器,是系统内的控制寄存器之一。控制寄存器是一些特殊的寄存器,它们可以控制CPU的一些重要特性。0位是保护允许位PE(Protedted Enable),用于启动保护模式,如果PE位置1
,则保护模式
启动,如果PE=0
,则在实模式
下运行。PG=1,则为启动分页。
jmpi 0, 8:把0赋给ip,把8赋给cs,查找gdt中的8。下面就用CS=8,在GDT中查找对应的地址。
上述为GDT中CS=8,查到的表中的内容。
对应到GDT的结构位置是:
段基址31..24 段基址23..16
4| 00 C0 | 9A 00 |
0| 0000 | 07FF |
段基址15..0 段限长15..0
0x00C09A00000007FF,将字放置在对应的低位和高位位置上。按照保护模式,取到的段基址其实是0x0000,那么这句话就是跳转到地址为0x00000的地方开始,使用jmp后就会跳转到内存0x0000处执行,而该地址处存放的便是system模块。
拓展资料:
详解操作系统启动
linux0.11—setup.s模块
你管这叫操作系统(二)
操作系统之GDT和IDT(三)
head.s是system中的第一个模块,jmpi 0, 8就是跳到了head.s去执行。
源码通过Makefile编译后产生操作系统镜像Image,通过指令或工具将镜像放到0磁道0扇区。当我们再次启动操作系统时,就会从0磁道0扇区中读取我们的镜像文件,然后读取bootset.s执行,再读取setup和system模块,再执行后续代码。
Image依赖于boot/bootsect、boot/setup、tools/system…这几个文件。bootsect依赖于bootsetc.s、setup依赖于setup.s等等。
tools/system依赖于boot/head.o、init/main.o…(驱动程序、内存管理等等)。
上述模块拥有之后,就执行链接操作**$(LD),将上述模块链接在一起形成tools/system**。
拓展:gcc编译C源码步骤
预处理 ——> 编译 ——> 汇编 ——> 链接
(1)预处理:宏定义展开、头文件展开、条件编译等,同时将代码中的注释删除,这里并不会检查语法。将 .c 文件处理后输出 .i 文件。
(2)编译:检查语法,将预处理后文件编译生成汇编文件。将 .i 汇编成用汇编语言写的 .s 文件。
(3)汇编:将汇编文件生成目标文件(二进制文件)。将 .s 文件编译成 .o 文件。
(4)链接:C语言写的程序是需要依赖各种库的,所以编译之后还需要把库链接到最终的可执行程序中去。将编译输出的 .o 文件链接成可执行文件。
c语言编译过程及工程下的.c文件.h文件.o文件.so文件.a文件
之前配置的idt
和gdt
只用于jmpi 0, 8
指令实现跳转,现在再次初始化idt
和gdt
,用于后续真正的执行。
开启了20号地址线,就可以开始访问4GB内存。
在这里使用了三种汇编语法:as86、GNU as和内嵌汇编。
head.s跳出来之后,会跳到main.c去执行(汇编跳到c函数)。
head.s进行压栈,从上至下,由高地址到低地址入栈:0、0、0、L6、_main。其中0、0、0会作为main的三个参数,L6会作为返回地址。当执行完push后,会跳到set_paging(设置页表)执行。当setup_paging执行完ret( } )之后,会从栈顶开始执行main() 函数,执行完后返回地址为L6,而L6对应的又是jmp L6
从而出现了一种死循环的局面,造成死机。所以main()永远不会返回,也因此操作系统是一个永不停止的程序。
因if(!fork()){init(); }
永远不会退出,所以**main()**是一个永远不会退出的程序。
输入:0x90002处的内存大小信息。
mem_init执行内存的初始化,记录内存中那些地方是使用的那些地方是没使用的。初始化mem_map
数组,每次右移12位(4K),清0,每4K内存作为一个页。
(1)BIOS执行某些系统的检测,并在物理地址0处开始初始化中断向量。
(2)BIOS启动设备的第一个扇区,将bootsect.s读入到绝对地址0x7C00处,并跳转到这个地方。
(3)bootsect.s程序开始执行,并将自己移动到0x90000位置处,然后把setup.s代码读入到0x90200处,再把system模块读入到0x10000开始处
(4)setup.s函数执行,获取内存、物理硬件等信息,将其放在0x90000-0x90200处(覆盖bootsect.s程序)。然后,将system模块从0x10000-0x8ffff移动到0x00000完后的位置。最后,进入保护模式,实现从16位机的20位寻址方式变为32位机的32位寻址方式,然后再跳转到system模块开始执行。
(5)执行system中的第一个模块head.s,加载IDT、GDT以及LDT,调用init/main.c中的 main() 程序。
环境搭建参考:
Linux 0.11 实验环境搭建
注:如果是从电脑上拷贝到Ubuntu的话需要给setup.h
加上可执行文件权限
chmod +x setup.h
chmod +x gcc-3.4-ubuntu.tar.gz
chmod +x hit-oslab-linux-qiuyu.tar.gz
参考资料:
操作系统实验一到实验九合集(哈工大李治军)
! 以 16 进制方式打印栈顶的16位数
print_hex:
! 4 个十六进制数字
mov cx,#4
! 将(bp)所指的值放入 dx 中,如果 bp 是指向栈顶的话
mov dx,(bp)
print_digit:
! 循环以使低 4 比特用上 !! 取 dx 的高 4 比特移到低 4 比特处。
rol dx,#4
! ah = 请求的功能值,al = 半字节(4 个比特)掩码。
mov ax,#0xe0f
! 取 dl 的低 4 比特值。
and al,dl
! 给 al 数字加上十六进制 0x30
add al,#0x30
cmp al,#0x3a
! 是一个不大于十的数字
jl outp
! 是a~f,要多加 7
add al,#0x07
outp:
int 0x10
loop print_digit
ret
! 这里用到了一个 loop 指令;
! 每次执行 loop 指令,cx 减 1,然后判断 cx 是否等于 0。
! 如果不为 0 则转移到 loop 指令后的标号处,实现循环;
! 如果为0顺序执行。
!
! 另外还有一个非常相似的指令:rep 指令,
! 每次执行 rep 指令,cx 减 1,然后判断 cx 是否等于 0。
! 如果不为 0 则继续执行 rep 指令后的串操作指令,直到 cx 为 0,实现重复。
! 打印回车换行
print_nl:
! CR
mov ax,#0xe0d
int 0x10
! LF
mov al,#0xa
int 0x10
ret