protected mode

保护模式简介

实模式的缺陷
  1. 实模式下操作系统与用户程序属于听一个特权级,容易引起系统崩溃。

  2. 程序引用的地址指向真实的物理地址,即逻辑地址等于物理地址,不利于内存分片管理,容易造成内存碎片化。

  3. 用户程序可以随意访问任意内存。

  4. 一个短只能访问64KB地址,太小了,操作不方便。

  5. 一次只能运行一个程序。

  6. 只有20根地址线,只能寻址1M的空间,太小啦。

为了克服实模式低劣的内存管理方式,开发出了保护模式。

保护模式的特点
  1. 寄存器扩展。
  2. 寻址空间扩展为4GB。
  3. 对内存分段并进行描述。全局描述符表,全局描述符表寄存器,段描述符缓冲寄存器。
进入保护模式

进入保护模式的步骤为:

  1. 打开A20
  2. 加载gdt
  3. 将cr0的pe位置1
  4. jmp 刷新流水线
1
2
3
4
5
6
7
8
9
10
11
12
13
;-----------------  打开A20  ----------------
in al,0x92
or al,0000_0010B
out 0x92,al

;----------------- 加载GDT ----------------
lgdt [gdt_ptr]


;----------------- cr0第0位置1 ----------------
mov eax, cr0
or eax, 0x00000001
mov cr0, eax

内存分段

段描述符的结构
作用
高32位
31-24 段基址31-24
23 G
22 D/B
21 L
20 AVL
19-16 段界限19-16
15 P
14-13 DPL
12 S
11-8 TYPE
7-0 段基址23-16
低32位
31-16 段基址15-0
15-0 段界限15-0
  1. 实际的段界限值=(描述符中的段界限值+1)*(段界限的粒度,1B 或者 4KB) - 1
  2. G=0,粒度为1B,G=1,粒度为4KB.
    3、E=0,向上扩展,常用于代码段与数据段。E=1,向下扩展,常用于栈段。
全局描述符表 GDT

全局描述符表相当于描述符的数组,每一个元素是8字节的描述符,使用选择子进行索引。GDT中的第0个描述符是不可用的。

全局描述符表寄存器 GDTR

GDT存在于内存中,GDTR是一个专门指向全局描述符表的寄存器。结构如下:

47-15 15-0
GDT初始地址 GDT界限

在实模式先使用lgdt加载描述符表,但是由于实模式的限制,此时的GTD只能处于1M地址空间以内,所以到了保护模式可以再次进行lgdt更改GDT的位置。

lgdt [48位内存数据]
按小端字节序,前16位用于舒适化GDT界限,后32位,用于指定GDT的初始地址。

选择子

在实模式下,段寄存器选择的是段基址,在开启内存分段后,段寄存器存储的是对应段描述符表的索引和相关数据。选择子的结构如下:

15-4 3 2-0
描述符索引值 TI RPL

描述符索引值正好为12位,刚好和GDTR界限所确定的最大描述符数量相匹配。

获得内存信息

eg:

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
;-------  int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 获取内存布局  -------

xor ebx, ebx ;第一次调用时,ebx值要为0
mov edx, 0x534d4150 ;edx只赋值一次,循环体中不会改变
mov di, ards_buf ;ards结构缓冲区
.e820_mem_get_loop: ;循环获取每个ARDS内存范围描述结构
mov eax, 0x0000e820 ;执行int 0x15后,eax值变为0x534d4150,所以每次执行int前都要更新为子功能号。
mov ecx, 20 ;ARDS地址范围描述符结构大小是20字节
int 0x15
jc .e820_failed_so_try_e801 ;若cf位为1则有错误发生,尝试0xe801子功能
add di, cx ;使di增加20字节指向缓冲区中新的ARDS结构位置
inc word [ards_nr] ;记录ARDS数量
cmp ebx, 0 ;若ebx为0且cf不为1,这说明ards全部返回,当前已是最后一个
jnz .e820_mem_get_loop

;在所有ards结构中,找出(base_add_low + length_low)的最大值,即内存的容量。
mov cx, [ards_nr] ;遍历每一个ARDS结构体,循环次数是ARDS的数量
mov ebx, ards_buf
xor edx, edx ;edx为最大的内存容量,在此先清0
.find_max_mem_area: ;无须判断type是否为1,最大的内存块一定是可被使用
mov eax, [ebx] ;base_add_low
add eax, [ebx+8] ;length_low
add ebx, 20 ;指向缓冲区中下一个ARDS结构
cmp edx, eax ;冒泡排序,找出最大,edx寄存器始终是最大的内存容量
jge .next_ards
mov edx, eax ;edx为总内存大小
.next_ards:
loop .find_max_mem_area
jmp .mem_get_ok

;------ int 15h ax = E801h 获取内存大小,最大支持4G ------
; 返回后, ax cx 值一样,以KB为单位,bx dx值一样,以64KB为单位
; 在ax和cx寄存器中为低16M,在bx和dx寄存器中为16MB到4G。
.e820_failed_so_try_e801:
mov ax,0xe801
int 0x15
jc .e801_failed_so_try88 ;若当前e801方法失败,就尝试0x88方法

;1 先算出低15M的内存,ax和cx中是以KB为单位的内存数量,将其转换为以byte为单位
mov cx,0x400 ;cx和ax值一样,cx用做乘数
mul cx
shl edx,16
and eax,0x0000FFFF
or edx,eax
add edx, 0x100000 ;ax只是15MB,故要加1MB
mov esi,edx ;先把低15MB的内存容量存入esi寄存器备份

;2 再将16MB以上的内存转换为byte为单位,寄存器bx和dx中是以64KB为单位的内存数量
xor eax,eax
mov ax,bx
mov ecx, 0x10000 ;0x10000十进制为64KB
mul ecx ;32位乘法,默认的被乘数是eax,积为64位,高32位存入edx,低32位存入eax.
add esi,eax ;由于此方法只能测出4G以内的内存,故32位eax足够了,edx肯定为0,只加eax便可
mov edx,esi ;edx为总内存大小
jmp .mem_get_ok

;----------------- int 15h ah = 0x88 获取内存大小,只能获取64M之内 ----------
.e801_failed_so_try88:
;int 15后,ax存入的是以kb为单位的内存容量
mov ah, 0x88
int 0x15
jc .error_hlt
and eax,0x0000FFFF

;16位乘法,被乘数是ax,积为32位.积的高16位在dx中,积的低16位在ax中
mov cx, 0x400 ;0x400等于1024,将ax中的内存容量换为以byte为单位
mul cx
shl edx, 16 ;把dx移到高16位
or edx, eax ;把积的低16位组合到edx,为32位的积
add edx,0x100000 ;0x88子功能只会返回1MB以上的内存,故实际内存大小要加上1MB

.mem_get_ok:
mov [total_mem_bytes], edx ;将内存换为byte单位后存入total_mem_bytes处。

内存分页

分段是分页的前提

内存分段模式下的问题
  1. 物理内存不足时怎么把?
  2. 内存碎片化问题怎么解决。

在保护模式下,段描述符是内存段的身份证,cpu根据一个段描述符来引用一个段。很多时候,段描述符对应的段并不在内存中。如果一个描述符的P位为1,则表示该段在内存中存在。访问过一个段后,cpu将该描述符中的A为置1。如果P位为0,则说明内存中不存在这个段,则cpu抛出np异常,操作系统将对应的段加载到内存中。

分页机制的思想是,通过映射,可以使连续的线性地址与任意物理内存地址相关联,逻辑上连续的额线性地址在物理上可以不连续。分页机制将大小不不等的段分解为大小相等的页,再将页映射到物理页。分页机制的作用有:将线性地址转化为物理地址,用大小相等的页代替大小不等的段。有了页表的映射关系,经过段部件的处理输出的则为虚拟地址。在分页机制下,每个进程都认为自己独享整个4GB空间。即程序以为自己身处于段式存储下并拥有4GB空间,但是他的地址经由页部件处理之后才是真正的物理地址。

一级页表

页表用于存储线性地址与物理地址之间的映射。页表中的每一项为大小为4字节的页表项,用来记录4GB空间的物理地址。当访问一个线性地址的时候,实际上就是在访问页表项中对应的物理地址。

每个页大小为4KB,一个页表可以存储1M个页表项,加起来一个页表可以表示整个4GB物理空间。

一级页表的地址转换过程为:用线性地址的高20位作为页表项的索引,每个页表项占用4字节的大小,索引值乘上4就可以得到该页表项在页表中的偏移量。用cr3寄存器中的页表物理地址加上此偏移量就可以得到该页表项的物理地址,从该页表项中得到映射的物理地址,再与低12位的线性地址相加就可以得到最终要访问的物理地址。

假设咱们是在平坦模型下工作,不管段选择子值是多少,其所指向的段基址都是 0,指令 mov ax,
[0x1234]中的 0x1234 称为有效地址,它作为“段基址:段内偏移地址”中的段内偏移地址。这样段基址
为 0,段内偏移地址为 0x1234,经过段部件处理后,输出的线性地址是 0x1234。由于咱们是演示分页机制,必须假定系统已经打开了分页机制,所以线性地址 0x1234 被送入了页部件。页部件分析 0x1234 的高20 位,用十六进制表示高 20 位是 0x00001。将此项作为页表项索引,再将该索引乘以 4 后加上 cr3 寄存器中页表的物理地址,这样便得到索引所指代的页表项的物理地址,从该物理地址处(页表项中)读取所映射的物理页地址:0x9000。线性地址的低 12 位是 0x234,它作为物理页的页内偏移地址与物理页地址0x9000 相加,和为 0x9234,这就是线性地址 0x1234 最终转换成的物理地址。

二级页表

二级页表将4GB空间按每一个标准页大小4KB分为1M个页,将这1M个页分1K*1K个页,每1K个页表项又正好可以组成一个新的页(1K*4B=4KB),将这个新产生的页记录为一个页表项,则总共会产生1K个新的页表项,再将这1K个页表项组成一个标准页,则最后产生的这个标准页为页目录表,其中每一项为页目录项PDE。

二级页表地址转换原理是将 32 位虚拟地址拆分成高 10 位、中间 10 位、低 12 位三部分,它们
的作用是:高 10 位作为页表的索引,用于在页目录表中定位一个页目录项 PDE,页目录项中有页表物理地址,也就是定位到了某个页表。中间 10 位作为物理页的索引,用于在页表内定位到某个页表项 PTE,页表项中有分配的物理页地址,也就是定位到了某个物理页。低 12 位作为页内偏移量用于在已经定位到的物理页内寻址。

转换过程背后的具体步骤如下。
(1)用虚拟地址的高 10 位乘以 4,作为页目录表内的偏移地址,加上页目录表的物理地址,所得的
和,便是页目录项的物理地址。读取该页目录项,从中获取到页表的物理地址。
(2)用虚拟地址的中间 10 位乘以 4,作为页表内的偏移地址,加上在第 1 步中得到的页表物理地址,
所得的和,便是页表项的物理地址。读取该页表项,从中获取到分配的物理页地址。
(3)虚拟地址的高 10 位和中间 10 位分别是 PDE 和 PTE 的索引值,所以它们需要乘以 4。但低 12 位就不是索引值啦,其表示的范围是 0~0xfff,作为页内偏移最合适,所以虚拟地址的低 12 位加上第 2 步中得到的物理页地址,所得的和便是最终转换的物理地址。

页目录项和页表项的结构

页目录项

31-12 11-9 8 7 6 5 4 3 2 1 0
页表物理页地址31-12位 AVL G 0 D A PCD PWT US RW P

页表项

31-12 11-9 8 7 6 5 4 3 2 1 0
物理页地址31-12位 AVL G PAT D A PCD PWT US RW P

页表目录项和页表项中的都是物理页地址,标准页大小就是4KB,所以地址都是4K的倍数,即低12位全为零,所以只需要记录高20位就可以了。省下来的12位可以用于其他属性。

P,Present,意为存在位。若为 1 表示该页存在于物理内存中,若为 0 表示该表不在物理内存中。操
作系统的页式虚拟内存管理便是通过 P 位和相应的 pagefault 异常来实现的。

RW,Read/Write,意为读写位。若为 1 表示可读可写,若为 0 表示可读不可写。

US,User/Supervisor,意为普通用户/超级用户位。若为 1 时,表示处于 User 级,任意级别(0、1、2、 3)特权的程序都可以访问该页。若为 0,表示处于 Supervisor 级,特权级别为 3 的程序不允许访问该页,该页只允许特权级别为 0、1、2 的程序可以访问。

PWT,Page-level Write-Through,意为页级通写位,也称页级写透位。若为 1 表示此项采用通写方式,表示该页不仅是普通内存,还是高速缓存。此项和高速缓存有关,“通写”是高速缓存的一种工作方式,本位用来间接决定是否用此方式改善该页的访问效率。这里咱们直接置为 0 就可以啦。

PCD,Page-level Cache Disable,意为页级高速缓存禁止位。若为 1 表示该页启用高速缓存,为 0 表示禁止将该页缓存。这里咱们将其置为 0。 A,Accessed,意为访问位。若为 1 表示该页被 CPU 访问过啦,所以该位是由 CPU 设置的。还记得段描述符中的 A 位和 P 位吗?这两位在一起可以实现段式虚拟内存管理。和它们一样,这里页目录项和页表项中的 A 位也可以用来记录某一内存页的使用频率(操作系统定期将该位清 0,统计一段时间内变成 1 的次数),从而当内存不足时,可以将使用频率较低的页面换出到外存(如硬盘),同时将页目录项或页表项的 P位置 0,下次访问该页引起 pagefault 异常时,中断处理程序将硬盘上的页再次换入,同时将 P 位置 1。 D,Dirty,意为脏页位。当 CPU 对一个页面执行写操作时,就会设置对应页表项的 D 位为 1。此项仅针对页表项有效,并不会修改页目录项中的 D 位。

PAT,Page Attribute Table,意为页属性表位,能够在页面一级的粒度上设置内存属性。比较复杂,将此位置 0 即可。

G,Global,意为全局位。由于内存地址转换也是颇费周折,先得拆分虚拟地址,然后又要查页目录,又要查页表的,所以为了提高获取物理地址的速度,将虚拟地址与物理地址转换结果存储在 TLB(Translation Lookaside Buffer)中,TLB 以后咱们会细说。在此先知道 TLB 是用来缓存地址转换结果的高速缓存就 ok 啦。此 G 位用来指定该页是否为全局页,为 1 表示是全局页,为 0 表示不是全局页。若为全局页,该页将在高速缓存 TLB 中一直保存,给出虚拟地址直接就出物理地址啦,无需那三步骤转换。由于 TLB 容量比较小(一般速度较快的存储设备容量都比较小),所以这里面就存放使用频率较高的页面。顺便说一句,清空 TLB 有两种方式,一是用 invlpg 指令针对单独虚拟地址条目清理,或者是重新加载 cr3 寄存器,这将直接清空 TLB。

AVL,意为 Available 位,表示可用,谁可以用?当然是软件,操作系统可用该位,CPU 不理会该位
的值,那咱们也不理会吧。

开启分页

开启分页需要顺序做以下三件事情。

  1. 准备好页目录表和页表。
  2. 将页表地址写入控制寄存器cr3。
  3. 寄存器的PG位置1.

elf (executable and linkable format)

elf中的数据类型
数据类型名称 字节大小 对齐 意义
Elf_Half 2 2 无符号中等大小的整数
Elf_Word 4 4 无符号大整数
Elf_Addr 4 4 无符号程序运行地址
Elf_Off 4 4 无符号文件偏移量

特权级

RPL:请求特权级指令请求访问其他资源的能力成为请求特权级,指令存放在代码段中,所所以使用CS中选择子的RPL位表示代码请求别人资源的能力。

CPL:表示处理器当亲的特权级。指令最终是由处理器执行的,执行到不同特权的代码,处理器的特权级就切换到不同的等级。代码段描述符中的DPL便是当前处理器所处的特权级。

对于数据段来讲,只有访问者的权限大于或等于段描述符中的DPL表示的最低权限时才能够访问。

对于代码段来讲,只有访问者的权限等于段描述符中的DPL才能访问。即只能平级访问。访问一个代码段实质上就是跳转到这个段进行执行。唯一一种处理器从高特权级降到低特权级执行的情况是处理器从中断处理程序中返回到用户态。

一致性代码段:一致性代码段也成为依从代码段,用来实现从低特权级的代码向高特权级代码的转移。一致性代码段是指如果自己是转移后的目标段,则自己的特权级一定能要大于等于转移前的CPL,也就是说一致性代码段的DPL是特权的上限。处理器遇到目标端位一致性代码段的时候并不会将CPL用该段的DPL来替换。代码段可以有一致性与不一致性的段,但是数据段只能有非一致性,即数据段不允许比自己特权级低的代码段访问。

函数调约定

cdecl (c declaration 即c声明)

函数参数从右到左顺序入栈,EAX,ECX,EDX,寄存器由调用者保存,其余的寄存器由被调用者保存。函数的返回值存储在EAX寄存器中。由调用者清理栈空间。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int subtract(int a, int b); //被调用者
int sub = subtract (3,2); // 主调用者
主调用者:
; 从右到左将参数入栈
push 2 ;压入参数 b
push 3 ;压入参数 a
call subtract ;调用函数 subtract
add esp, 8 ;回收(清理)栈空间
被调用者:
push ebp ;压入 ebp 备份
mov ebp,esp ;将 esp 赋值给 ebp
;用 ebp 作为基址来访问栈中参数
mov eax,[ebp+0x8] ;偏移 8 字节处为第 1 个参数 a
add eax,[ebp+0xc] ;偏移 0xc 字节处是第 2 个参数 b
;参数 a 和 b 相加后存入 eax
mov esp,ebp ;为防止中间有入栈操作,用 ebp 恢复 esp
;本句在此例子中可有可无,属于通用代码
pop ebp ;将 ebp 恢复
ret

C与汇编混合编程

c语言和汇编语言混合编程分为两种:

  1. 单独的汇编代码文件与单独的c语言文件分别编译成目标文件后,再进行连接成可执行程序。
  2. 再c语言中嵌入汇编语言,直接编译成生可执行程序。这种也叫做内联汇编。

内联汇编

基本内联汇编

格式:
asm [volatile](“asm code”)

  1. 指令必须要用双引号括起来
  2. 一对双引号不可以跨行,如果跨行需要在结尾使用’\‘转义。
  3. 指令之间使用’;’,’\n’,’\t’分隔开。
  4. 即使指令分隔在多个双引号中也要使用分隔符。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
 char* str="hello,world\n"; 
int count = 0;
void main(){
asm("pusha; \
movl $4,%eax; \
movl $1,%ebx; \
movl str,%ecx; \
movl $12,%edx; \
int $0x80; \
mov %eax,count;\
popa \
");
}

扩展内联汇编

格式:
asm [volatile] (“assembly code”:output : input : clobber/modify)

  1. assembly code:还是用户写入的汇编指令,和基本内联汇编一样。

  2. output:output 用来指定汇编代码的数据如何输出给 C 代码使用。内嵌的汇编指令运行结束后,如果想将运行结果存储到 c 变量中,就用此项指定输出的位置。

  3. input:input 用来指定 C 中数据如何输入给汇编使用。要想让汇编使用 C 中的变量作为参数,就要在此指定。

  4. clobber/modify:汇编代码执行后会破坏一些内存或寄存器资源,通过此项通知编译器,可能造成寄
    存器或内存数据的破坏,这样 gcc 就知道哪些寄存器或内存需要提前保护起来。

约束

  1. 寄存器约束

寄存器约束就是要求 gcc 使用哪个寄存器,将 input 或 output 中变量约束在某个寄存器中。常见的寄存器约束有:
a:表示寄存器 eax/ax/al
b:表示寄存器 ebx/bx/bl
c:表示寄存器 ecx/cx/cl
d:表示寄存器 edx/dx/dl
D:表示寄存器 edi/di
S:表示寄存器 esi/si
q:表示任意这 4 个通用寄存器之一:eax/ebx/ecx/edx
r:表示任意这 6 个通用寄存器之一:eax/ebx/ecx/edx/esi/edi
g:表示可以存放到任意地点(寄存器和内存)。相当于除了同 q 一样外,还可以让 gcc 安排在内存中
A:把 eax 和 edx 组合成 64 位整数
f:表示浮点寄存器
t:表示第 1 个浮点寄存器
u:表示第 2 个浮点寄存器

1
2
3
4
5
6
#include<stdio.h> 
void main() {
int in_a = 1, in_b = 2, out_sum;
asm("addl %%ebx, %%eax":"=a"(out_sum):"a"(in_a),"b"(in_b));
printf("sum is %d\n",out_sum);
}
  1. 内存约束

内存约束是要求 gcc 直接将位于 input 和 output 中的 C 变量的内存地址作为内联汇编代码的操作数,不需要寄存器做中转,直接进行内存读写,也就是汇编代码的操作数是 C 变量的指针。

m:表示操作数可以使用任意一种内存形式。
o:操作数为内存变量,但访问它是通过偏移量的形式访问,即包含 offset_address 的格式。

1
2
3
4
5
6
7
#include<stdio.h> 
void main() {
int in_a = 1, in_b = 2;
printf("in_b is %d\n", in_b);
asm("movb %b0, %1;"::"a"(in_a),"m"(in_b));
printf("in_b now is %d\n", in_b);
}
  1. 立即数约束

i:表示操作数为整数立即数
F:表示操作数为浮点数立即数
I:表示操作数为 0~31 之间的立即数
J:表示操作数为 0~63 之间的立即数
N:表示操作数为 0~255 之间的立即数
O:表示操作数为 0~32 之间的立即数
X:表示操作数为任何类型立即数

  1. 通用约束

0~9:此约束只用在 input 部分,但表示可与 output 和 input 中第 n 个操作数用相同的寄存器或内存。

AT&T汇编

intel 与 AT&T 语法风格对比

区别 intel AT&T 说明
寄存器 寄存器没有钱追你 寄存器有前缀%
操作数顺序 目的操作数在左边,源操作数在右边 相反
操作数指定大小 有关内存的操作数要加数据类型指定大小,byte:8位,word:16位,dword:32位 指令的最后一个字母代表指令操作数大小,b:8位,w:16位,l:32位。
立即数 没有前缀 有前缀$
远跳转 jmp far segment:offset ljmp $segment:$offset
远调用 call far segment:offset lcall $segment:$offset
远返回 ret far n lret $n

内存寻址格式:
segreg(段基址):base_address(offset,index,size)

segreg:base_address+offset+index*size

打印 printf 的实现

打印字符

在printf.h中声明函数

1
void put_char(uint8_t char_asci)

在print.s中完成函数的定义

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
96
97
98
99
100
101
global put_char
;外部符号声明
pushad ;备份8个32位寄存器
mov ax,SELECTOR_VIDEO
mov gs,ax

;获取光标位置
;高8位
mov dx,0x03d4 ;索引寄存器
mov al,0x0e ;光标位置高8位
out dx,al
mov dx,0x03d5
in al,dx
mov ah,al
;低8位
mov dx,0x03d4 ;索引寄存器
mov al,0x0f ;光标位置高8位
out dx,al
mov dx,0x03d5
in al,dx

;光标为值存放在bx中
mov ax,bx
;取得要打印的字符
mov ecx,[esp+36]
cmp cl,0xd
jz .is_carriage_return
cmp cl,0xa
jz .is_line_feed
cmp cl,0x8
jz .is_backspace
jmp .put_other

.is_backspace:
dec bx ;光标位置减一
shl bx,1 ;左移一位,bx乘二得到光标处字符所在为内存地址。
mov word [gs:bx],0 ;将这一个字符为位置清零包括颜色属性
shr bx,1 ;bx恢复原状
jmp .set_cursor

.put_other:
shl bx,1
mov [gs:bx],cl
inc bx
mov byte [gs:bx],0x07
shr bx,1
inc bx
;若光标值小于2000,则表示该页现存没有写完,若超出2000,则回车换行处理
cmp bx,2000
jl .set_cursor

.is_line_feed:
.is_carriage_return:
xor dx,dx
mov ax,bx
mov si,80
div si
sub bx,dx
;先让光标回到行首再下一行,判断是否超出。
.is_carriage_return_end:
add bx,80
cmp bx,2000
.is_line_feed_end:
jl .set_cursor

;超出屏幕大小开始滚屏
.roll_screen:
cld
mov ecx,960 ;共搬运 2000-80=1920个字符
mov esi,0xb80a0 ;第一行行首
mov edi,0xb8000 ;第零行行首
rep movsd
;将最后一行填充为空白
mov ebx,3840
mov ecx,80
.cls:
mov word [gs:ebx],0
add ebx,2
loop .cls
mov bx,1920

;设置光标值
.set_cursor:
;将光标设为bx值
mov dx, 0x03d4 ;索引寄存器
mov al, 0x0e ;用于提供光标位置的高8位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口0x3d5来获得或设置光标位置
mov al, bh
out dx, al

mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
mov al, bl
out dx, al

.put_char_done:
popad
ret
打印字符串

在printf.h中声明函数

1
void put_str(char* message)

在print.s中完成函数的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
global put_str
put_str:
;由于本函数中只用到了ebx和ecx,只备份这两个寄存器
push ebx
push ecx
xor ecx, ecx ; 准备用ecx存储参数,清空
mov ebx, [esp + 12] ; 从栈中得到待打印的字符串地址
.goon:
mov cl, [ebx]
cmp cl, 0 ; 如果处理到了字符串尾,跳到结束处返回
jz .str_over
push ecx ; 为put_char函数传递参数
call put_char
add esp, 4 ; 回收参数所占的栈空间
;由调用者回收栈空间,C语言调用的话编译器会自动完成但是汇编语言不会,需要我们手动回收。
inc ebx ; 使ebx指向下一个字符
jmp .goon
.str_over:
pop ecx
pop ebx
ret
打印整数

在printf.h中声明函数

1
void put_int(uint32_t num);

在print.s中完成函数的定义

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
global put_int
put_int:
pushad
mov ebp,esp
mov eax,[ebp+36]
mov edx,eax
mov edi,7
mov ecx,8 ;32位数字共分为8块
mov ebx,put_int_buffer

.16based_4bits:
and edx,0x0000000F
cmp,9
jg .isA2F
add edx,'0'
jmp .store
.is_A2F:
sub edx,10
add edx,'A'
;从大往小存储在缓冲区内,最后一个数字放在最高位
.store:
mov [ebx+edi],dl
dec edi
shr eax,4
mov edx,4
loop .16based_4bits

.ready_to_print:
inc edi
.skip_prefix_0 ;判断是不是8位全零
cmp edi,8
je .full0
.go_on_skip:
mov cl,[put_int_buffer+edi]
inc edi
cmp cl,'0'
je .skip_prefix_0
dec edi
jmp .put_each_num

.full0:
mov cl,'0'
.put_each_num:
push ecx
call put_char
add esp,4
inc edi
mov cl,[put_int_buffer+edi]
cmp edi,8
jl .put_each_num
popad
ret

杂项问题

  1. 对于push指令,处于对齐的要求,操作数要么是16位要么是32位,所以8位操作数会被扩展为运行模式下的默认操作数宽度。实模式为16位,保护模式为32位。
  2. 使用伪指令 [bits 16] [bits 32] 指定编译器进行模式指定。
  3. 操作数反转前缀 0x66 寻址方式反转前缀 0x67