目錄
回顧
上一篇,我們講到了以下內容:
- 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
- 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