目录
回顾
上一篇,我们讲到了以下内容:
- 16-bit Real Mode 是 x86 系列 CPU 的一种工作模式,所有 x86 系列 CPU 启动时,都处于 16-bit Real Mode
- 16 bit 系统中,只有 64 KB 内存可以被识别,CPU 一次只能处理 16 bit 的数据
- 8086 架构,因为有 20 bit 的地址总线,所以寻址总量有 1 MB
- 8086 架构的内存管理采用内存分段机制,内存分段将程序不同的部分加载到不同的不连续的内存中
- 16-bit Real Mode 真实物理地址的计算公式为:物理地址 = 段选择符左移 4 位 + 偏移地址
- 中断可以让 CPU 暂时停止当前任务,执行我们指定的任务,然后回去执行原先的任务
- 中断在 interrupt vector 中由一个数值做索引,interrupt vector 包含 interrupt service routines 的内存地址
- 第一个 Hello World 程序使用中断,在屏幕上显示 HelloWorld 字符串
今日目标
本篇,我们将结束 16-bit Real Mode 的内容,储备更多的知识,为下一篇开启 32-bit Protected Mode 做准备。
本篇内容主要以汇编代码为主,讲解一些必要的指令。我们会从一个打印 16 进制字符串的程序开始,将
- boot sector 与内存的关系
- 字符串定义
- 堆栈的使用
- 方法调用
- 条件跳转
- 文件包含
- 物理内存寻址
这些内容一一覆盖到。
需要大家注意的是,我们现在的编程环境和编程的内容都有很大的局限性,所以不要将现在所讲的东西不加推敲直接应用到其他环境中,会造成不正确的结果。
比如现在的环境只是 8086 16-bit,那么不能认为通用寄存器就只是 AX,而不去考虑是否因为环境不同,寄存器会发生变化。32-bit 环境下,通用寄存器为 EAX,64-bit 环境下,为 RAX。
又比如目前的 boot sector 程序没有分段,同样不能认为其他所有的汇编程序都不用分段。
当前讨论的,是如何在 8086 架构下,从头写一个简易操作系统,一切都围绕这个目标来展开。
Boot Sector 与内存的关系
我们从研究 boot sector 程序与内存的关系开始。
回想之前,计算机启动,BIOS 做自检(专业术语叫 POST - Power-On Self-Test,详见这篇文章),然后读取存储介质上的前 512 个字节,如果 0x55 和 0xaa 出现在第 510 和 511 个字节上,BIOS 就认为这是一个正规的 boot sector,开始执行其代码,然后加载操作系统。
一切程序的执行,第一步要将程序加载到内存。那么 boot sector 被 BIOS 加载到内存的哪一部分呢?
首先,肯定不是整个内存的起始位置(0x0)。因为之前说过,远在 BIOS 寻找 boot sector 之前,就已经开始做检测硬件等的操作。想必有一些必要的指令,已经被加载到内存最初的位置上。另外,还记得 interrupt vector table
以及 interrupt service routines (ISRs)
,这些都必须首先存在于内存,我们在 boot sector 中才有能力去调用中断,做相应的操作。
书中说,boot sector,会被 BIOS 固定加载到 0x7c00
这个位置上。
直接引用书上的内存示意图(来源:https://www.cs.bham.ac.uk/~exr/lectures/opsys/10_11/lectures/os-dev.pdf)。
原书中作者写着写着打了一个问号在这里。本着怀疑一切的态度,我们要证实一下,boot secotr 是不是被加载到了 0x7c00
这个位置。
打印任意内存位置上的内容(以 16 进制输出)
我们有能力打印字符(上一个 HelloWorld 程序),那么要证实 boot sector 被加载到 0x7c00
,只需要打印出该内存地址开始的连续一些字节,和 od
命令的输出做比对,就能知道书中是否正确。记住 16-bit Real Mode 下内存没有保护机制,所以,是可以访问或者写入到任意内存,目前也没有什么后果,大家可以自行尝试。
这里的关注点不要放在代码上,后面会对代码做深入讨论。
打印 16 进制的代码在 @cfenollosa 的 Github 上能找到。这里需要两个文件,一个是 print
方法,负责打印字符串,另一个,是 print_hex
方法,负责转换 16 进制数为字符串,然后调用 print
来打印。
print.asm:
print:
pusha
; keep this in mind:
; while (string[i] != 0) { print string[i]; i++ }
; the comparison for string end (null byte)
start:
mov al, [bx] ; 'bx' is the base address for the string
cmp al, 0
je done
; the part where we print with the BIOS help
mov ah, 0x0e
int 0x10 ; 'al' already contains the char
; increment pointer and do next loop
add bx, 1
jmp start
done:
popa
ret
print_nl:
pusha
mov ah, 0x0e
mov al, 0x0a ; newline char
int 0x10
mov al, 0x0d ; carriage return
int 0x10
popa
ret
print_hex.asm:
; receiving the data in 'dx'
; For the examples we'll assume that we're called with dx=0x1234
print_hex:
pusha
mov cx, 0 ; our index variable
; Strategy: get the last char of 'dx', then convert to ASCII
; Numeric ASCII values: '0' (ASCII 0x30) to '9' (0x39), so just add 0x30 to byte N.
; For alphabetic characters A-F: 'A' (ASCII 0x41) to 'F' (0x46) we'll add 0x40
; Then, move the ASCII byte to the correct position on the resulting string
hex_loop:
cmp cx, 4 ; loop 4 times
je end
; 1. convert last char of 'dx' to ascii
mov ax, dx ; we will use 'ax' as our working register
and ax, 0x000f ; 0x1234 -> 0x0004 by masking first three to zeros
add al, 0x30 ; add 0x30 to N to convert it to ASCII "N"
cmp al, 0x39 ; if > 9, add extra 8 to represent 'A' to 'F'
jle step2
add al, 7 ; 'A' is ASCII 65 instead of 58, so 65-58=7
step2:
; 2. get the correct position of the string to place our ASCII char
; bx <- base address + string length - index of char
mov bx, HEX_OUT + 5 ; base + length
sub bx, cx ; our index variable
mov [bx], al ; copy the ASCII char on 'al' to the position pointed by 'bx'
ror dx, 4 ; 0x1234 -> 0x4123 -> 0x3412 -> 0x2341 -> 0x1234
; increment index and loop
add cx, 1
jmp hex_loop
end:
; prepare the parameter and call the function
; remember that print receives parameters in 'bx'
mov bx, HEX_OUT
call print
popa
ret
HEX_OUT:
db '0x0000',0 ; reserve memory for our new string
利用 cfenollosa 代码,我们写一个简单的测试代码:
boot_sect_mem_chk.asm
org 0x7c00
mov dx, [0x7c00] ; 将内存地址 0x7c00 位置上的数据,写入到 dx 寄存器
call print_hex ; 调用 print_hex 方法输出 dx 中的内容
jmp $ ; 跳转到当前地址(无限循环)
%include "print.asm"
%include "print_hex.asm"
times 510 - ($ - $$) db 0 ; 512 个字节中剩余字节全部填充 0
dw 0xaa55 ; 最后两个字节,是 0xaa55,让 BIOS 知道这是 boot sector
编译:
nasm -f bin boot_sect_mem_chk.asm -o boot_sect_mem_chk.bin
运行:
与 od
命令的输出做对比:
od -t x1 -A n boot_sect_mem_chk.bin
证实了 boot sector 被加载到 0x7c00
内存的说法。也可以写一个循环,输出前 N 个字符,对比输出与 od
命令是完全一致的。
不要忘了,x86 架构是小字节序,QEMU 中打印出来的每个字节,与 od
命令显示的是相反的。
物理寻址的应用
前一篇文章铺垫了很多关于 8086 架构物理地址计算的信息,现在,该尝试一下在代码中做应用,加深理解。
找到 Boot Sector 在内存中的前两个字节的位置
我们还是拿 boot_sect_mem_chk.asm 举例,顺便开始讲解一些必须知道的汇编指令。
我们的目标是,用前文所说的寄存器,换一种方式,使用段寄存器来找到 Boot Sector 的前两个字节的内容,打印出来。
org 0x7c00
mov dx, [0x7c00] ; 将内存地址 0x7c00 位置上的数据,写入到 dx 寄存器
call print_hex ; 调用 print_hex 方法输出 dx 中的内容
jmp $ ; 跳转到当前地址(无限循环)
%include "print.asm"
%include "print_hex.asm"
times 510 - ($ - $$) db 0 ; 512 个字节中剩余字节全部填充 0
dw 0xaa55 ; 最后一个字节,是 0xaa55,让 BIOS 知道这是 boot sector
我们先讲一些完成这个任务的必要知识,然后完成这个任务,再然后,我们来拆解这个程序,进入汇编指令的讲解。
寄存器
如果有人问说计算机有哪些寄存器?你的回答只是计算机有 AX,BX,CX,DX 等,然后就没有然后了。那么,再次了解一下寄存器。
最基本的寄存器描述如下:
由于 CPU 访问内存的操作会大大降低计算机运行效率,所以 CPU 内部内置了一些记忆单元用于临时储存要处理的小量数据,这些记忆单元,就被称之为寄存器。
寄存器的分类
x86 架构下,按照寄存器的作用,可以把寄存器分为 5 大类,分别是:
- 通用寄存器 General Registers
- 段寄存器 Segment Registers
- 指针寄存器 Pointer Registers
- 索引寄存器 Index Registers
- 控制寄存器 Control Registers
x86 架构的 CPU 包含 4 个通用寄存器,32 位架构下,这 4 个寄存器分别是:
EAX
,EBX
,ECX
,EDX
。
相应的,我们现在讨论的 80806 16 位架构下,这 4 个寄存器分别是:
- AX:'A' 代表 Accumulator,该寄存器也称为累积寄存器,通常用于做数学运算
- BX:'B' 代表 Base,该寄存器也称为基址寄存器,可以用于存放临时值,也可以用于做内存寻址
- CX:'C' 代表 Count,该寄存器也称为计数寄存器,通常用于做循环计数
- DX:'D' 代表 Data,该寄存器也称为数据寄存器,通常用于输入输出的操作,同时也可以和 AX 配合用于大数值乘除的运算
对于段寄存器的讲解比较重要,粗体字加深理解。
8086 CPU 有 3 个常用段寄存器,3 个附加段寄存器。
常用段寄存器分别是:
- 代码段寄存器 Code Segment Register:代码段包含所有的可以被执行的指令;代码段寄存器存放代码段的起始内存地址(逻辑地址)
- 数据段寄存器 Data Segment Register:数据段包含程序需要的所有数据,常量,字符串等;数据段寄存器存放数据段的起始内存地址(逻辑地址)
- 栈段寄存器 Stack Segement Register:栈段包含方法调用所需的参数,方法的返回地址等数据;栈段寄存器存放栈段的起始内存地址(逻辑地址)
逻辑地址指的是 16-bit 地址,也是后面段和段寄存器小结中提到的段选择符。
例如,DS 的值是 0x6F70
,那么 mov ax, [0x1000] 中的物理内存地址是
0x6F70 * 10H + 0x1000 = 0x6F700 + 0x1000 = 0x70700
附加段寄存器分别是:
ES
,FS
,GS
。这些寄存器提供了额外储存数据的空间。通常,MOVS
,CMPS
等字符串操作的会用 ES
。程序员也可以在代码中手动指定这些寄存器的使用。
3 个指针寄存器分别是:
- 指令指针寄存器 Instruction Pointer(IP):存放下一条指令的内存地址偏移量,与 CS 一起,[CS:IP] 提供下一条指令的真实物理地址
- 栈指针寄存器 Stack Pointer(SP):存放栈中的当前数据的内存地址偏移量,与 SS 一起,[SS:SP] 提供当前数据的真实物理地址,获得当前位置上的数据
- 基址指针寄存器 Base Pointer(BP):存放栈底的内存地址偏移量,与 SS 一起,[SS:BP] 提供当前方法参数的真实物理地址;同时,BP 还可以跟 DI,SI 这两个索引寄存器一起使用,用于物理地址计算,如 [BP + SI + 0x10]
2 个指针寄存器分别是:
- 源索引寄存器 Source Index(SI):常用于字符串操作的源索引
- 目标索引寄存器 Destination Index(DI):常用于字符串操作的目标索引
不过实际中,这两个寄存器也常与段寄存器配合,进行物理地址寻址,如 [DS:SI],[ES:DI]。
控制寄存器不在讨论范围。有兴趣的同学参考这篇文章。
段和段寄存器小结
8086 架构中,内存段十分重要,所以我们把段和段寄存器拿出来做一个小总结。
关于每一个段的特征以及每个段与内存寻址的关系,总结如下:
- 8086 架构下,每个段容量最大 64 KB(216 Bits)
- 汇编中,任何段中的任一内存地址的寻址,都是相对于段的起始内存地址
- 一个段,总是开始于一个能被 16 整除(10 进制,或者 16 进制被 10H 整除)的内存地址上
- 任一段寄存器存放的,都是相应段的起始内存地址。段寄存器中的地址,被称为*段选择符(Segment Selector 这里有详解),段选择符 16 (16 进制乘以 10H,二进制左移 4 位)之后的,才是段地址(所以,上一篇文章勘误,直接说段寄存器中储存的是段地址是错误的表述)**
- 物理地址是通过 段地址 + 偏移量 计算得到
来自 Wiki 的图片,解释物理地址的计算(二进制形式)。
完成任务
好了,有了上面的铺垫,我们来使用段寄存器找到 boot sector 的前两个字节。
首先,我们讲 org
指令注释掉。汇编的注释,使用分号 ;
。
我们看一下注释掉之后是个什么情况,还能打印出我们需要的前两个字节吗?
答案是否定的。
为什么会出现这样的情况?
先看一下这条指令
mov dx, [0x7c00]
在 []
操作符中的地址,都是一个相对于段起始内存地址的内存偏移量。
当我们使用
mov dx, [0x7c00]
的时候,事实上,assembler 内部是这样处理这个内存寻址的
mov dx, [DS:0x7c00]
DS 是段寄存器,我们会使用段寄存器中的段选择符,乘以 16 再加上 0x7c00 这个偏移量来计算最后的物理地址。
那我们来看一下没有 org 指令的情况下,DS 寄存器的值是多少?
我们尝试将 DS 的值打印出来,结果发现没有输出,意味着 DS 中没有存放任何值(没有值不代表是 0)。
因此,无法找到 boot sector 前两个字节是理所当然的。
解决方案就很简单了,存放一个段选择符到 DS 中即可。
前文说过,汇编中所有寻址,都是相对于段的起始地址而言。我们已经知道并证实 boot sector 代码会被加载到 0x7c00
这个位置,我们的目标是打印物理地址位 0x7c00
这个位置上的数据。
根据
物理地址 = 段选择符 * 10H + 偏移量
的公式,DS 的值应该是 0x7c0
,偏移量应该是 0x0
。
0x7c0 * 10H + 0x0 = 0x7c00
因此,修改代码如下,即可得到 boot sector 前两个字节。
这里有一个点要说明一下,所有的段寄存器和索引寄存器都是不能直接写入的,mov ds, 0x7c0
无法编译通过,必须有一个中间过渡,所以用 ax
作为过渡,将值写入到 ds
中。
拓展部分
另外,再抛出一个问题,为什么不能设置 DS 寄存器的值为其他值,如0x0
,或者0x100
,然后设置偏移量到相应的值去获取前两个字符。我目前的解释是,因为我们编译出来的是原始字节文件,意味着所有字节都是数据,那么将被加载到 1 个段中(一个段 64 KB,我们的数据只有 512 Bytes),段的起始逻辑地址,就应该被写入到 DS 中,所有的对于段中字节的寻址,都以 DS 为相对地址。
为了证实这一点,我们可以获取一下最后两个字节0xaa55
。按照公式,DS 为0x7c0
,偏移量为0x1fe
(第 510 和 511 个字节)。可以成功获取到最后两个字节的内容。
当然,我试过将 DS 设置为 0x0,偏移为 0x7c00 会有很奇怪的现象出现,大家自行尝试。
程序拆解及必要汇编指令
拆解一下前文的程序,代码如下:
org 0x7c00
mov dx, [0x7c00] ; 将内存地址 0x7c00 位置上的数据,写入到 dx 寄存器
call print_hex ; 调用 print_hex 方法输出 dx 中的内容
jmp $ ; 跳转到当前地址(无限循环)
%include "print.asm"
%include "print_hex.asm"
times 510 - ($ - $$) db 0 ; 512 个字节中剩余字节全部填充 0
dw 0xaa55 ; 最后一个字节,是 0xaa55,让 BIOS 知道这是 boot sector
【1】:org
指令,明确告诉 assembler 我们的 Boot Sector 代码被加载到 0x7c00
的位置
【2】:mov
指令,将相对于 0x0,偏移量为 0x7c00 内存位置上的值写入 dx
【3】:调用 print_hex
方法,打印出 dx 寄存器中的值
【4】:挂起 CPU
【5】【6】:包含两个打印方法的文件
【7】:除去该行指令以上所有指令的长度,除去最后两个字节的长度,其余位置全部填充 0
【8】:最后两个字节固定值,0xaa55
org 指令
这个解释只有英文才能区分了,不知道该怎么翻译才好,中文的翻译都是指令。各种资料对于 org
的解释,指出 org
不是一个 instruction
,而是一个 directive
。类似 C 语言中的 define
。
org
指令,在书中的解释是,明确告诉 assembler 我们的 Boot Sector 代码被加载到指定的位置(0x7c00)。但是范范这么一句话,感觉什么都没有讲明白。根据资料,assembler 内部,有一个 Location Counter(LC),它负责记录当前内存中下一个可以用于存放编译后指令的空位。
org
指令更改 LC 到指定的内存地址。例如这里的 0x7c00
。
当前的 LC 的值,可以使用之前看到过的 $
符号来表示。
我们可以打印出来看一下。
没有使用 org
指令,LC 的值是 0000
(打印出来的乱码至今未理解是为什么)
使用了 org
指令,LC 的值是指定值。
LC 的值,是根据指令递增的,一条指令被编译存放入上一个 LC 的位置之后,LC 会增加这个指令的长度,准备存放下一个指令。
下图展示了 LC 在第一个 call print_hex
指令之后,递增了 3 个字节。
LC 作为概念大家明白就行。我试着找 LC 与内存寻址相关的资料,结果都没有找到。意味着 LC 对于我们理解内存寻址来说没有什么作用。
但是我们可以通过程序来的行为来进一步解释 org 指令到底干了什么。
我们可以打印寄存器中的值,那么,我就把所有寄存器的值全部打印出来,然后对比一下使用 org 指令和没有使用 org 指令前后的区别,看是否是因为 org 指令初始化了 DS 寄存器,让 mov dx, [0x7c00]
可以获取到相应数据。
对比结果如下图。
使用了 org 指令,除了 sp 是一个非 0 值,其他所有寄存器都被设置成了 0x0。因此,根据寻址公式,0x0 * 10H + 0x7c00 = 0x0 + 0x7c00 = 0x7c00
,就可以获取到 boot sector 前两个字节的内容。
然后我将 org 指令注释掉,结果是各个寄存器全部是乱码。最后也无法获取到 boot sector 前两个字节的内容。
可以初步得出结论,org 指令除了资料上说的设置 LC 到当前地址,还初始化段寄存器的值为 0x0
。
jmp 指令
jmp $
在这里的作用,是做一个无限循环,让 CPU 停在该指令处,不能再往下执行。
试想一下如果没有这个无限循环,CPU 就会按着 CS:IP 一路往下执行,能执行的则执行,不能执行的就 crash,我们不想让这样的事情发生。
jmp 跳转分 short jump,long jump,还分向前跳转,向后跳转。细看这个 jmp $
指令,机器码是 eb fe
,还有很多可以挖掘的地方,它是一个 short jump,是一个 reverse short jump,意思是向后跳转。具体操作是从 jmp $
紧接着的下一个指令的地址开始算,往回跳转两个字节,因为 eb fe
就是两个字节,所以跳回指令本身,造成一个无限循环。
详细不展开,关于 jmp $
指令,好文一篇。
times 指令
重复执行后面的操作 N 次。在 boot sector 程序中,最后两个字节固定,因此,填充 0 的操作应该进行 times 指令当前的地址,减去段起始地址的结果这么多次。
$
操作符代表当前指令地址,$$
代表段起始地址。
pusha popa 指令
调用方法的时候,如果方法不小心修改了寄存器的值,可能会造成意想不到的结果。我们不希望寄存器的值被修改,有一种办法是在调用方法之前,将所欲寄存器的值以及方法的返回地址都 push 到栈中,然后调用结束,再全部 pop 回来。
这样的操作很麻烦,所以 pusha 和 popa 指令,会帮助我们完成这一操作。pusha 在方法中调用一次,会将所有寄存器以及方法返回地址都保存到栈,popa 则执行相反的操作。
print_hex:
pusha
...
popa
ret
条件控制指令
条件跳转跟在 CMP 指令之后使用,如:
cmp ax, 0x4
je jump_point
jump_point:
do_something_here
最常用的是下面几个:
- JE 如果相等,跳转
- JNE 如果不相等,跳转
- JG 如果大于目标,跳转
- JGE 如果大于等于目标,跳转
- JL 如果小于目标,跳转
- JLE 如果小于等于目标,跳转
其他必要的汇编指令
最重要的部分已经讲完了,接下来,用示例代码的方式带过剩余简单的部分。
- 字符串定义
最后的 0,添加一个 null byte
作为字符串结尾
HELLO:
db 'Hello, World', 0
GOODBYE:
db 'Goodbye', 0
- 文件包含
关于文件包含,有一个问题还没有解决,就是为什么文件包含要写在 jmp $
指令之后。我试过将他们放在文件其他地方,确实会发生无法预料的结果。有待研究。
%include "boot_sect_print.asm"
%include "boot_sect_print_hex.asm"
- 堆栈的使用
关于栈,记住几个点即可。
第一,后进先出 (LIFO);第二,BP 寄存器指向栈底,SP 寄存器指向栈顶;第三,栈从内存高位地址向低位地址增长;第四,push 一个值到栈,SP - 2;pop 一个值出栈,SP + 2
可以使用书中的代码进行理解。
mov ah, 0x0e ; int 10/ ah = 0eh -> scrolling teletype BIOS routine
mov bp, 0x8000 ; Set the base of the stack a little above where BIOS
mov sp, bp ; loads our boot sector - so it won ’t overwrite us.
push 'A' ; Push some characters on the stack for later
push 'B' ; retreival. Note , these are pushed on as
push 'C' ; 16 - bit values , so the most significant byte
; will be added by our assembler as 0 x00.
pop bx ; Note , we can only pop 16 - bits , so pop to bx
mov al, bl ; then copy bl ( i.e. 8- bit char ) to al
int 0x10 ; print (al)
pop bx ; Pop the next value
mov al, bl
int 0x10 ; print (al)
mov al , [0x7ffe ] ; To prove our stack grows downwards from bp ,
; fetch the char at 0 x8000 - 0x2 ( i.e. 16 - bits )
int 0x10 ; print (al)
jmp $ ; Jump forever.
; Padding and magic BIOS number.
times 510 - ($ - $$) db 0
dw 0xaa55
总结
- Boot Sector 被 BIOS 加载到 0x7c00 的内存位置
- 用程序证实了 0x7c00 物理内存位置上,确实是我们的 Boot Sector 程序
- 寄存器的分类,以及寄存器的作用
- 用段寄存器来完成寻找 Boot Sector 前两个字节内容的任务
- 必要的汇编指令
下一篇,我们将讲解如何读取磁盘数据,之后就要开启我们的 32-bit Protected Mode 之旅。
- https://wiki.osdev.org/Boot_Sequence
- https://www.cs.bham.ac.uk/~exr/lectures/opsys/10_11/lectures/os-dev.pdf
- https://nets.ec/Ascii_shellcode
- https://thestarman.pcministry.com/asm/debug/Segments.html
- https://www.tutorialspoint.com/assembly_programming/assembly_registers.htm
- http://dewkumar.blogspot.com/2012/01/what-is-org-origin-directive-in.html#:~:text=What%20is%20ORG%20(origin)%20directive%20in%20assembly%20level%20language%3F,expression%20in%20the%20operand%20field.
- https://stackoverflow.com/questions/38318230/how-to-display-register-value-using-int-10h
- https://en.wikipedia.org/wiki/INT_13H#INT_13h_AH.3D02h:_Read_Sectors_From_Drive
- https://stackoverflow.com/questions/50260277/is-there-a-difference-between-org-0x7c00-and-mov-ax-07c0h
- https://thestarman.pcministry.com/asm/2bytejumps.htm
- http://ps-2.kev009.com/wisclibrary/aix52/usr/share/man/info/en_US/a_doc_lib/aixassem/alangref/absolute_add.htm
- http://et.engr.iupui.edu/~skoskie/ECE362/lecture_notes/LN2_html/text8.html
- https://en.m.wikipedia.org/wiki/X86_memory_segmentation
- https://en.wikipedia.org/wiki/Segment_descriptor
- http://ref.x86asm.net/coder32.html
- https://www.tortall.net/projects/yasm/manual/html/objfmt-bin.html
- http://www.sce.carleton.ca/courses/sysc-3006/s13/Lecture%20Notes/Part5-SimpleAssembly.pdf
- https://stackoverflow.com/questions/4903906/assembly-using-the-data-segment-register-ds
- https://wiki.osdev.org/Real_Mode
- https://www.daniweb.com/programming/software-development/threads/291076/whats-org-100h#:~:text=ORG%20(abbr.,only%20one%20segment%20of%20max.
- https://www.nasm.us/doc/nasmdoc3.html#section-3.5
- https://www.tutorialspoint.com/assembly_programming/assembly_conditions.htm