前面應該有一章,“一:操作系統的概述”,懶得寫,但是很重要,最好去看下視頻,如果有人看的話,以后有空再補
首先我們要有一個認知,就是計算機是怎么運行的
從白紙到圖靈機
計算機開始的時候就是一個做計算的機器
那我們從人計算的過程來思考
比如說,我們在紙上看到 3 + 2
大腦算出結果是5
那就用筆在紙上寫上5這個答案
那我們用一個自動設備來模擬這個過程
紙帶模擬紙
控制器模擬大腦
讀寫頭來模擬眼睛和筆
這里控制里的表是固定的,比如它只能進行加法運算
你給3和5,它只會算出8
從圖靈機到通用圖靈機
上面說的圖靈機,控制器里面的邏輯是固定的,就像一個只會做一道菜的廚子,他的腦子里只夠裝下一個道菜的做法,不會學習新的菜譜。
那么通用圖靈機就是一個可以看懂菜譜的廚師了。
他的控制器就像這個聰明的廚師的大腦,一直處于一個求知的狀態。每看到一道菜譜,就做一道菜。控制器從紙帶中載入一個新的控制器動作,啟動這個動作后,后面獲取的數據,就是在這套邏輯下開始運行。比如載入一個qq的邏輯,那么控制器就在給出qq的邏輯判斷,你點發送,它就知道你是要把這消息發送過去。
從通用圖靈機到計算機
馮·諾依曼存儲程序思想
將程序和數據存放到計算機內部的存儲器中,計算機在程序的控制下一步一步進行處理
存儲器:那個厚厚的菜譜
IP:就像你看菜譜時的手指,慢慢的往下劃,告訴自己我正在操作這一步,等你操作完這一步,你的手指就會劃到下一步,告訴自己要執行下一步了。
IR:就像你小小的大腦,當你的手指指到那過程的時候,你就記住這個指令,然后一直默默記住這個指令,轉身去執行他,所以IR就是存儲IP里指到的指令
CPU[運算器,控制器]:就是你大腦,當你看到“把油倒到鍋里”這六個字的時候,你知道它的實際意思,就是把油倒到鍋里,是不是覺得這樣說很傻,那如果我寫put oil to the pan,如果你沒學過英語你就根本不知道這是什么意思,如果你學過英語就知道,它的實際意思,就是把油倒到鍋里。
mov ax, [100]:mov 是 就像是put,把A放到B那里,那ax就是the pan,[100] 就是oil, 那這句話的意思就是:put the [100] to the ax
所以說:計算機就一個永不停歇的苦力,這要一開始的時候,我們告訴他從哪里開始做,他就會一直一條一條的執行下去
打開電源,計算機執行的第 一句指令什么?
x86 PC
(1) x86 PC剛開機時CPU處于實模式
(2)開機時,CS=0xFFFF; IP=0x0000
(3)尋址0xFFFF0(ROM BIOS映射區)
(4) 檢查RAM,鍵盤,顯示器,軟硬磁盤
(5) 將磁盤0磁道0扇區讀入0x7c00處
(6) 設置cs=0x07c0,ip=0x0000
如果我就這么列出來,你們肯定是不懂的啦!我們大概可以知道,剛開機的時候,電腦從某個地方取指然后開始執行。
下面我就一個一個解釋:
- x86
也就是8086,是CPU的一種型號,比如8086的上一代機就是8085,8080。為什么要指定說是x80PC呢,因為不同型號的CPU的結構是不一樣的。比如說8085,8080是8位機,而8086是16位機,也可以說是8086是16位結構的CPU。那什么是16位結構的CPU呢?
- 運算器一次最多可以處理16位的數據;
- 寄存器的最大寬度為16位;
- 寄存器和運算器之間的通路為16位;
也就是說,在8086內部,能夠一次性處理,傳輸,暫時存儲的信息的最大長度是16位的。內存單元的地址在送上地址總線之前,必須在CPU中處理,傳輸,暫時存放,對于16位CPU,能一次性處理,傳輸,暫時存儲16位的地址。
- CS和IP
我們剛剛說過IP就是你的手指來指定一個地址的地方,那CS又是什么呢?
我們剛剛說完8086的CPU一次只能處理16位,但是它可是有20位的地址總線,就好比說,你的車可以載20噸的土,你的挖掘機一次可以挖16噸的土,你會只讓車裝了16噸就走了嗎?不會,你一定會利用它還有4噸可以裝,讓它裝滿再讓它走的。那8086CPU就采用一種在內部用兩個16位地址合成的方法來形成一個20位的物理地址。
image.png
也就是: 段地址X16 + 段偏移地址 = 物理地址
image.png
- 段地址X16
其實就是左移4位,這里的位指的是二進制的位,像圖中給的1230其實是16進制的,比如這里的十六進制的0,實際上就是二進制的0000,二進制左移了4位,也就是16進制的0向左移動一個下。那原本1230,向左移動一下,就是12300再加上00C8,就可以得出了123C8了
我們再明確一點,CS和IP兩個的寄存器,結合起來指示了CPU當前要讀取指令的地址。
綜上,可得
CS:代碼段寄存器
IP:指令寄存器
等等,要是有人不知道寄存器的話,我只能說,真是正個人被你打敗了呢。
image.png
這里的方格就好比一個寄存器,你看第二個方格里寫了9,第三個方格里與第四個方格,合起來是13,那,你怎么想都知道,一個方格只能寫0~9,不可能寫出一個11吧。那下面的16位寄存器就應該明了了,寄存器就一個暫時存放數據的地方
16位寄存器的邏輯結構
那計算機里只能存1和0,那這里面的方格就只能存1和0咯
image.png
都說到這里了,我們就順便說下,不同的CPU,寄存器的個數,結構是不想同的。那8086CPU有14個寄存器,每個寄存器有一個名稱,我們可以給他們分類
- 通用寄存器
- 控制寄存器
段寄存器
image.png
等等有人要吐槽我剛剛的分類了,說怎么沒有指令寄存器,隨便分的嘛,你大概知道是干啥的就好了嘛,又不是要考試。你沒看我圖都是到處亂截的嗎?
這里要說下通用寄存器AX,BX,CX,DX,細心的同學發現了他們是可以再分的分成一個AH和AL,H就是高的意思,那L就是低咯
image.png
為什么要這樣呢?還記得我剛剛說過8086前面有8085,8080的CPU嗎?我說過他們是8位機,也就是他們只能處理8位,所以為了兼容他們,我們就把AX再細分了一下,這樣,我們就可以通過AH傳送一個8位的數據了,不然你傳輸16位,他們是識別不全的,就會造成混亂。
后面要是都遇到什么寄存器,再慢慢說吧!我怕你們都快忘記這是一個操作系統教程了。我們就先不說實模式是什么了,因為這樣還要說到保護模式,我們就暫缺忽略先。
那此時在看這圖的時候,我們就知道了,剛開機的時候
CS=0xFFFF,IP=0x000,那么我們可以知道CPU現在指向內存的0xFFFF0處,也就是圖中的ROM BIOS映射區那
- BIOS
也就是基本輸入/輸入程序拉,英文你就自己想嘛,base input output system??隨便打的,不知道對不對?尷尬!我們就只要知道,它是固化在內存里面的,因為我們說過CPU是一個苦力,會一定不斷的執行一條條步驟,那前提是,你要告訴他第一條是在哪里,他才會不停的做下去,BIOS就是他第一件要做的事,那這事就是計算機開機時執行系統各部分的自檢,建立起系統需要使用的各種配置表,并且把處理器和系統其余本分初始化到一個已知狀態,等等。有人會問,那ROM BIOS和ROM BIOS
映射區是啥區別?因為會設計到兼容等問題,我就不說了,你只要知道,ROM BIOS存放著我剛剛說的那些功能的代碼,到時,那些代碼會被復制到這個映射區,并被CPU執行。
那第4就不用說了咯,第5就有點意思了。
那這個0磁盤0扇區就是一個512k的引導扇區了。
這時候CS=0x7c00,IP=0x000
那就是CPU會在0x7C00處取指執行
那。。。
終于要開始代碼了。
0x7c00處存放的代碼
接下來代碼,我會先放一份源碼,其余的是抽取出來重要的代碼。第一份看是有個整體的認知,別的就是摘取一些重要的代碼分析,并不是全部,只是一些主干的代碼
源碼:boot/bootsect.s
!
! SYS_SIZE is the number of clicks (16 bytes) to be loaded.
! 0x3000 is 0x30000 bytes = 196kB, more than enough for current
! versions of linux
!
SYSSIZE = 0x3000
!
! bootsect.s (C) 1991 Linus Torvalds
!
! bootsect.s is loaded at 0x7c00 by the bios-startup routines, and moves
! iself out of the way to address 0x90000, and jumps there.
!
! It then loads 'setup' directly after itself (0x90200), and the system
! at 0x10000, using BIOS interrupts.
!
! NOTE! currently system is at most 8*65536 bytes long. This should be no
! problem, even in the future. I want to keep it simple. This 512 kB
! kernel size should be enough, especially as this doesn't contain the
! buffer cache as in minix
!
! The loader has been made as simple as possible, and continuos
! read errors will result in a unbreakable loop. Reboot by hand. It
! loads pretty fast by getting whole sectors at a time whenever possible.
.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text
SETUPLEN = 4 ! nr of setup-sectors
BOOTSEG = 0x07c0 ! original address of boot-sector
INITSEG = 0x9000 ! we move boot here - out of the way
SETUPSEG = 0x9020 ! setup starts here
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).
ENDSEG = SYSSEG + SYSSIZE ! where to stop loading
! ROOT_DEV: 0x000 - same type of floppy as boot.
! 0x301 - first partition on first drive etc
ROOT_DEV = 0x306
entry _start
_start:
mov ax,#BOOTSEG
mov ds,ax
mov ax,#INITSEG
mov es,ax
mov cx,#256
sub si,si
sub di,di
rep
movw
jmpi go,INITSEG
go: mov ax,cs
mov ds,ax
mov es,ax
! put stack at 0x9ff00.
mov ss,ax
mov sp,#0xFF00 ! arbitrary value >>512
! load the setup-sectors directly after the bootblock.
! Note that 'es' is already set up.
load_setup:
mov dx,#0x0000 ! drive 0, head 0
mov cx,#0x0002 ! sector 2, track 0
mov bx,#0x0200 ! address = 512, in INITSEG
mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors
int 0x13 ! read it
jnc ok_load_setup ! ok - continue
mov dx,#0x0000
mov ax,#0x0000 ! reset the diskette
int 0x13
j load_setup
ok_load_setup:
! Get disk drive parameters, specifically nr of sectors/track
mov dl,#0x00
mov ax,#0x0800 ! AH=8 is get drive parameters
int 0x13
mov ch,#0x00
seg cs
mov sectors,cx
mov ax,#INITSEG
mov es,ax
! Print some inane message
mov ah,#0x03 ! read cursor pos
xor bh,bh
int 0x10
mov cx,#24
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#msg1
mov ax,#0x1301 ! write string, move cursor
int 0x10
! ok, we've written the message, now
! we want to load the system (at 0x10000)
mov ax,#SYSSEG
mov es,ax ! segment of 0x010000
call read_it
call kill_motor
! After that we check which root-device to use. If the device is
! defined (!= 0), nothing is done and the given device is used.
! Otherwise, either /dev/PS0 (2,28) or /dev/at0 (2,8), depending
! on the number of sectors that the BIOS reports currently.
seg cs
mov ax,root_dev
cmp ax,#0
jne root_defined
seg cs
mov bx,sectors
mov ax,#0x0208 ! /dev/ps0 - 1.2Mb
cmp bx,#15
je root_defined
mov ax,#0x021c ! /dev/PS0 - 1.44Mb
cmp bx,#18
je root_defined
undef_root:
jmp undef_root
root_defined:
seg cs
mov root_dev,ax
! after that (everyting loaded), we jump to
! the setup-routine loaded directly after
! the bootblock:
jmpi 0,SETUPSEG
! This routine loads the system at address 0x10000, making sure
! no 64kB boundaries are crossed. We try to load it as fast as
! possible, loading whole tracks whenever we can.
!
! in: es - starting address segment (normally 0x1000)
!
sread: .word 1+SETUPLEN ! sectors read of current track
head: .word 0 ! current head
track: .word 0 ! current track
read_it:
mov ax,es
test ax,#0x0fff
die: jne die ! es must be at 64kB boundary
xor bx,bx ! bx is starting address within segment
rp_read:
mov ax,es
cmp ax,#ENDSEG ! have we loaded all yet?
jb ok1_read
ret
ok1_read:
seg cs
mov ax,sectors
sub ax,sread
mov cx,ax
shl cx,#9
add cx,bx
jnc ok2_read
je ok2_read
xor ax,ax
sub ax,bx
shr ax,#9
ok2_read:
call read_track
mov cx,ax
add ax,sread
seg cs
cmp ax,sectors
jne ok3_read
mov ax,#1
sub ax,head
jne ok4_read
inc track
ok4_read:
mov head,ax
xor ax,ax
ok3_read:
mov sread,ax
shl cx,#9
add bx,cx
jnc rp_read
mov ax,es
add ax,#0x1000
mov es,ax
xor bx,bx
jmp rp_read
read_track:
push ax
push bx
push cx
push dx
mov dx,track
mov cx,sread
inc cx
mov ch,dl
mov dx,head
mov dh,dl
mov dl,#0
and dx,#0x0100
mov ah,#2
int 0x13
jc bad_rt
pop dx
pop cx
pop bx
pop ax
ret
bad_rt: mov ax,#0
mov dx,#0
int 0x13
pop dx
pop cx
pop bx
pop ax
jmp read_track
!/*
! * This procedure turns off the floppy drive motor, so
! * that we enter the kernel in a known state, and
! * don't have to worry about it later.
! */
kill_motor:
push dx
mov dx,#0x3f2
mov al,#0
outb
pop dx
ret
sectors:
.word 0
msg1:
.byte 13,10
.ascii "Loading system ..."
.byte 13,10,13,10
.org 508
root_dev:
.word ROOT_DEV
boot_flag:
.word 0xAA55
.text
endtext:
.data
enddata:
.bss
endbss:
重點代碼:boot/bootsect.s
.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text
………………………………
SETUPLEN = 4 ! nr of setup-sectors
BOOTSEG = 0x07c0 ! original address of boot-sectors
INITSEG = 0x9000 ! we move boot here - out of the way
SETUPSEG = 0x9020 ! setup starts here
…………………………
entry _start
_start:
mov ax,#BOOTSEG
mov ds,ax
mov ax,#INITSEG
mov es,ax
mov cx,#256
sub si,si
sub di,di
rep
movw
jmpi go,INITSEG
go: mov ax,cs
mov ds,ax
mov es,ax
……………………
load_setup:
mov dx,#0x0000 ! drive 0, head 0
mov cx,#0x0002 ! sector 2, track 0
mov bx,#0x0200 ! address = 512, in INITSEG
mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors
int 0x13 ! read it
jnc ok_load_setup ! ok - continue
mov dx,#0x0000
mov ax,#0x0000 ! reset the diskette
int 0x13
j load_setup
大概說一下匯編
這里的匯編,源操作數在后面,目標操作數在前面
mov指令格式
注意到一點就是,不可以直接把數據放到段寄存器,所以我們都是先把數據放到通用寄存器,再把通用寄存器的值賦到段寄存器
- 通用寄存器
8086有4個通用寄存器:
AX――累加器(Accumulator),使用頻度最高
BX――基址寄存器(Base Register),常存放存儲器地址
CX――計數器(Count Register),常作為計數器
DX――數據寄存器(Data Register),存放數據- 段寄存器:
8086有6個段寄存器:只有兩個是特殊的CS和SS,CS講過了,SS后面再講
那剩下的四個就是DS,ES,GS和GS;當指令中沒有指定所操作數據的段時,那么DS將會是默認的數據段寄存器。
BOOTSEG = 0x07c0
INITSEG = 0x9000
mov ax,#BOOTSEG
mov ds,ax
mov ax,#INITSEG
mov es,ax
那就是
ds=07c0
es=9000
image.png
add是加法,sub是減法,還是遵循源操作數在后面,目標操作數在前面
比如 :sub ax ,8 如果ax原本的值是5,那就是5+8=13,然后把13放到ax中
sub si,si
sub di,di
自己減自己,當然就是零呀!所以si,di都是0.
我們前面說過,CPU的地址是由段地址和偏移地址組成的,就是這個圖
我們只知道段地址,是無法確定一個地址的,所以還需要兩個偏移地址,那就是si和di了。
ds:si = 7c00
es:di = 9000
他們就是這么配對的,不要問我為什么di不和ds在一起?我也不知道!記住就好了。
mov cx,#256
rep !重復執行并遞減cx的值,直到cx = 0 為止
movw !即movs指令。這里是從內存[si]處移動cx個字到[di]處
那就是移動256個字,256個字就是512個字節。
為什么是256個字,那是因為CX=256;
CX――計數器(Count Register),常作為計數器
計算機的字長決定了其CPU一次操作處理實際位數的多少.那我們說過8086是16位的CPU,那就是說這里1字=16位=2字節。因為一般1字節=8位。
這里的512k,是不是很熟悉,我們剛剛說過了引導扇區是512k,并我們知道movw指令是把內存[si]處的512k移動到了[di]處,[si]處地址就是7c00,也就是我們一開始存放bootsetct.s的地方。
jmpi go,INITSEG
go: mov ax,cs
mov ds,ax
mov es,ax
看圖我們知道bootserct.s已經移到了90000處,那我們的說過CPU的指令是根據cs和ip所指的地方執行的,這時候內存只有9000有代碼,我們當然在移動代碼后,要讓CPU指向他呀。這段代碼就是這個作用
jmp為無條件轉移指令,可以只修改IP,也可以同時修改CS和IP。
那jmpi是段間跳轉指令,也是同樣的道理
INITSEG = 0x9000
……
jmpi go,INITSEG !cs:INITSEG,ip:go
我們就知道這時候,CPU要執行的代碼地址:段地址為 INITSEG,即cs=9000,那ip=go?那go呢?
go是一個標號,我們就要講下標號的概念
標號實際上就是一個匯編的地址,匯編后,go就是從代碼執行開始的地方,經過了的偏移量
image.png
好像有點難懂是不是?我們先跳出來,講下為什么要有它,再反過來思考它的意思?
舉個例子,我們在看一本書,比如說《百年孤獨》,我是在宿舍看的!現在看到了第200頁。好!這時候,上課了,我還想繼續看,我把這本書帶到了教室,那我到教室后,是不是還是打開這本書,然后翻到第200頁。
那我們剛剛說過,我們把原本在7c00處的bootsect.s代碼移動到了9000處,bootsect.s就像這本書,我們在7c00處的時候,已經執行過了幾段代碼了,就像我在宿舍已經看了一些了,那當這代碼移動到9000處,就像我拿到教室了,那我還要繼續看,當然要從我上次看到的地方開始看呀!那代碼也是,要從上次執行到的地方開始執行,那上次執行到哪里了呢!就是執行到了
_start:
mov ax,#BOOTSEG
mov ds,ax
mov ax,#INITSEG
mov es,ax
mov cx,#256
sub si,si
sub di,di
rep
movw
jmpi go,INITSEG
go: mov ax,cs
mov ds,ax
mov es,ax
go標記的這個地方呀!所以go就是我看過的頁數!總結來說,書就是我的段地址,標號就是我翻過的頁數。
那我們現在就也明白了,為什么要有jmpi和go的存在了,實際上就是還是這段代碼繼續往下執行,只是因為我們剛剛把這代碼換了一個位置。
好了,我們折騰了這么多,總結成一句話,就是我們把磁盤的第一扇區(0磁道0扇區)中的一個512k的bootsect.s代碼復制到了內存的7c00處,還沒執行多少步,我們又把它移動到了9000處,然后繼續執行后面的代碼!那后面的代碼呢?
我們先看下
再來一張圖,告訴我們等等要干什么!
綜上,我們就知道,我們要把磁盤有4個扇區,辣么大的setup模塊,即setup.s移動到已經位于內存9000處的bootsect.s后面,我們說過bootsect.s是512k,那地址是多少呢,
我們的地址都是16進制的哦!所以我們就知道我們應該把setup.s移動到90200處!
SETUPLEN = 4 ! nr of setup-sectors
……………………
jmpi go,INITSEG !cs=9000
go: mov ax,cs !ds,es也等于9000
mov ds,ax
mov es,ax
………………
load_setup:
mov dx,#0x0000 ! drive 0, head 0
mov cx,#0x0002 ! sector 2, track 0
mov bx,#0x0200 ! address = 512, in INITSEG
mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors
int 0x13 ! read it
jnc ok_load_setup ! ok - continue
mov dx,#0x0000
mov ax,#0x0000 ! reset the diskette
int 0x13
j load_setup
0x13是BIOS讀磁盤扇區的中斷: 我們后面再講中斷,我們只要知道,就是CPU停下現在的工作,去做另一個工作就行了!
ah=0x02-讀磁盤,
al= 扇區數量(SETUPLEN=4),對應 mov ax,#0x0200+SETUPLEN SETUPLEN=4 那al=04
ch=柱面號, 對應 mov cx,#0x0002 , 那就是ch=00
dh=磁頭號, 對應 mov dx,#0x0000 , 那就是 dh=00
cl=開始扇區, 對應mov bx,#0x0200了,那就是es:bx=內存地址90200處了
dl=驅動器號, 對應mov dx,#0x0000,那就是dl=00
在我們把我們讀取磁盤的必要信息都存儲在寄存器后,我們就調用了int 0x13中斷,電腦就會到那里去執行讀磁盤的操作,并從剛剛賦值的寄存器中獲取必要的信息,那我們就把setup.s移到了復制到內存中的bootsect.s后面去了。
讀入setup模塊后: ok_load_setup
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).
……………………
int 0x13
j load_setup
ok_load_setup:
! Get disk drive parameters, specifically nr of sectors/track
mov dl,#0x00
mov ax,#0x0800 ! AH=8 is get drive parameters
int 0x13
mov ch,#0x00
seg cs
mov sectors,cx
mov ax,#INITSEG
mov es,ax
! Print some inane message
mov ah,#0x03 ! read cursor pos
xor bh,bh
int 0x10
mov cx,#24
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#msg1
mov ax,#0x1301 ! write string, move cursor
int 0x10
! ok, we've written the message, now
! we want to load the system (at 0x10000)
mov ax,#SYSSEG
mov es,ax ! segment of 0x010000
call read_it
………………
sectors:
.word 0
msg1:
.byte 13,10
.ascii "Loading system ..."
.byte 13,10,13,10
我們就主要講下那個打印的那段代碼吧
mov ah,#0x03 ! read cursor pos
xor bh,bh
int 0x10 !讀光標
mov cx,#24
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#msg1
mov ax,#0x1301 ! write string, move cursor
int 0x10 !顯示字符
…………
msg1:
.byte 13,10
.ascii "Loading system ..."
.byte 13,10,13,10
我們就猜下吧,第一次的int 0x10這個中斷,去獲取了光標的位置,然后我們再把msg1這個地址賦給了bp,而這個地址在下面有寫,看起來就是一個字符串,那再調用int 0x10的時候,就把這串字符顯示在剛剛獲取的光標位置那里。這里的int 0x10,我們先不要太糾結,我們可以思考成是一個嵌套函數,我們突然遇到這個函數,就跑過去執行,再加上因為參數的不同,他就會執行不一樣的代碼!有點像java的重載。也沒必要去背,如果真的要自己寫的話,到時一定有使用手冊來說明每個中斷代碼分別如何使用。
這里做的工作,就像我們打開PC時
這個是一樣的,只是別人有點高級,是動畫效果的呢!我們就是顯示字符串“Loading system……"
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).
………………
mov bp,#msg1
mov ax,#0x1301 ! write string, move cursor
int 0x10
! ok, we've written the message, now
! we want to load the system (at 0x10000)
mov ax,#SYSSEG
mov es,ax ! segment of 0x010000
call read_it !讀入system模塊
我們再往下分析。
還記得我上次發的一張圖嗎?
這樣我們就很容易知道了吧,先讓ax=0x1000,調用了一個read_it的函數,我們就可以猜,
這代碼八成是把原本磁盤上setup.s后面的代碼拷貝到內存的0x1000處。那我們就大概的看一下這個函數
read_it //讀入system模塊
SETUPSEG = 0x9020 ! setup starts here
………………
! ok, we've written the message, now
! we want to load the system (at 0x10000)
mov ax,#SYSSEG
mov es,ax ! segment of 0x010000
call read_it !讀入system模塊
………………
jmpi 0,SETUPSEG
………………
read_it:
mov ax,es
test ax,#0x0fff
die: jne die ! es must be at 64kB boundary
xor bx,bx ! bx is starting address within segment
rp_read:
mov ax,es
cmp ax,#ENDSEG ! have we loaded all yet?
jb ok1_read
ret
ok1_read:
seg cs
mov ax,sectors
sub ax,sread
mov cx,ax
shl cx,#9
add cx,bx
jnc ok2_read
je ok2_read
xor ax,ax
sub ax,bx
shr ax,#9
ok2_read:
………………
ok1_read:
seg cs
mov ax,sectors
sub ax,sread
mov cx,ax
shl cx,#9
add cx,bx
jnc ok2_read
je ok2_read
xor ax,ax
sub ax,bx
shr ax,#9
首先我們看過啟動盤里面代碼存放的圖,知道system模塊,是一個好長好長的代碼,所以復制過來是一件很麻煩的事,比如說代碼好長,磁道都變了,等等復制出錯了,檢查一下有沒有復制錯呀?一堆事要做,所以我們調用了一個read_it函數來完成這個艱巨的任務,它怎么實現的,我們就先別理了!
有趣的是,我們又看到了一個熟悉的身影
SETUPSEG = 0x9020 ! setup starts here
……
jmpi 0,SETUPSEG
一看他,我們就知道CPU要執行的地方,又開始發生變化了?;仡櫼幌拢?/p>
jmpi 偏移地址,段地址
那我們就知道CPU要到0x90200去執行代碼了!
好了,上面這張圖,我引用了很多次,就是想告訴你,我們說了這么多,實際上,就是完成了這一點點功能。攤手!
實驗一:修改開機的字符串
好了!大概就說到這里了,我們還有很多沒說,比如一開始的實模式是什么?還有中斷呀?還有剛剛call read_it 我們都說的語焉不詳,但是沒關系,一開始我們不要弄那么多,不然太容易迷失在代碼中,對操作系統的整體概念卻反而沒有具體的認識,后面會慢慢說到這篇沒有具體說到。
在下一章之前,我們來做個實驗練練手:更改剛剛啟動時的字符串,把Loading system 改成自己的名字,如wcdaren's os is loading
需要值的一提的就是
mov ah,#0x03 ! read cursor pos
xor bh,bh
int 0x10 !讀光標
mov cx,#24 !表示字符串的長度
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#msg1
mov ax,#0x1301 ! write string, move cursor
int 0x10 !顯示字符
…………
msg1:
.byte 13,10
.ascii "Loading system ..."
.byte 13,10,13,10
就是cx表示表示字符串的長度,到時如果我們寫的字符串要是過長,一定要記得設置cx的值。
匯編知識補充:int 和 call
在這段代碼的時候我們說因為system模塊可能很大,要跨越磁道,我們調用了 read_track 這個函數來復制該函數。調用函數,在C語言的時候我們是學個學過的。就是調用完這個函數后,回到原代碼處,繼續往下執行。但是,那匯編是如何完成的呢?
! ok, we've written the message, now
! we want to load the system (at 0x10000)
mov ax,#SYSSEG
mov es,ax ! segment of 0x010000
call read_it
call kill_motor
………………
read_it:
mov ax,es
test ax,#0x0fff
我們先不用知道read_it這個函數到底是如何實現,我們先前說過
jmpi go,INITSEG
go: mov ax,cs
mov ds,ax
mov es,ax
知道go是一個標號,即一個地址,這個地址是代碼開始到該標號的偏移量。
那我們就可以推出read_it也是一個標號。
那call read_it,我們一看就知道是跳到read_it,這里去執行。
這些我們都能理解,可我們講go的跳轉的時候,用的是jmpi
在說到jmpi的時候,我們說它跳到那9000處后,繼續執行9000那邊的代碼(一條一條的執行下去)。
但是我們的call,就不一樣了哦!他執行完了read_it后就會回到原來的地方執行下一條指令,即call kill_motor。
我們思考,計算機一定是有個地方,來存放當前的地址,等到那邊的代碼執行完了,就會來查看那存地址的地方,再跳回去。這就是棧了。
棧
棧:是一種具有特殊的訪問方式的存儲空間。它的特殊性就在于,最后進入這個空間的數據,最先出去。
我們用一個盒子和3本書來類比。
一個開口的盒子看成一個??臻g。現有有3本書,我們把他們放到盒子中,操作的過程如圖。
問題來了,如果我們一次只能拿一本書,我們如何將3本書從盒子中取出來?
顯然,必須從盒子的最上邊取,取的順序為:軟件工程,C語言,高等數學,和放入的順序相反。
從程序化的角度來講,應該有一個標記,這個標記一直指示著盒子最上邊的書。
如果說,上例中的盒子就是一個棧,我們可以看出,棧兩個基本的操作:入棧和出棧,入棧就是加一個新的元素放到棧頂,出棧就是從棧頂取出一個元素。棧頂元素總是最后入棧,需要出棧時又最先被從棧中取出,棧的這種操作規則被稱為:LIFO(Last In First Out,后進先出)。
現在的CPU都有棧的這種設計,并提供相關的指令以棧的方式訪問內存空間:PUSH(入棧)和POP(出棧)。比如,push ax 表示將寄存器ax中的數據送入棧中,pop ax 表示從棧頂取出的數據送入ax。8086CPU的棧操作都是以字為單位進行的。
下面舉例說明,我們把10000H~1000F這段內存當作棧來使用。
mov ax,0123H
push ax
mov bx,2266H
push bx
mov cx,1122H
push cx
pop ax
pop bx
pop cx
那我們如何告訴CPU我們把10000H~1000F這段內存當作棧呢?還有它怎么知道棧頂元素是什么呢?
先前,我們提到CS和IP,來定位一個地址。那棧也是如此的!8086CPU中,有兩個寄存器,段寄存器SS和寄存器SP,棧頂的段地址存儲在SS中, 偏移地址存儲在SP中。
任意時刻,SS:SP指向棧頂元素
舉例,push ax的執行,由以下兩步
- SP = SP - 2, SS:SP指向當前棧頂前面的單元,以當前棧頂前的單元為新的棧頂;
-
將ax中的內容送入 SS:SP指向的內存單元處,SS:SP此時指向新的棧頂。
image.png
call
回到call的講解。
CPU執行call指令時,進行兩步操作:
- 將當前的IP或CS和IP壓入棧中;
- 轉移
call 指令有很多中格式,我們這里就單獨那 call 標號 舉例
- (sp) = (sp) - 2
((ss)*16 + (sp)) = (IP) - (IP) = (IP) + 16位移
16位位移 = 標號處的地址 - call指令后的第一個字節的地址;
16位位移的范圍為-32768~32767,用補碼表示;
16位位移由編譯程序在編譯時算出。
其實,簡單來說就是
push IP
jmp near ptr 標號
哈哈,說到這更搞笑了,你們可能連jmp near ptr 是啥都不知道。
jmp near ptr 標號 的功能為:(IP) = (IP) + 16位移
- 16位位移 = 標號處的地址 - jmp指令后的第一個字節的地址
- near ptr 指名此處的位移為16位位移,進行的是段內近轉移;
- 16位位移的范圍為-32768~32767,用補碼表示;
- 16位位移由編譯程序在編譯時算出
好了,我們確實把等等要回去的地址存儲了,那,什么時候回去呢?也就是說回去的地址什么時候賦值回CS呢?
ret和retf
ret指令用棧中的數據,修改IP的內容,從而實現近轉移;
retf指令用棧中的數據,修改CS和P的內容,從而實現遠轉移。
CPU執行ret指令時,進行下面兩步操作
(1) (IP) = (ss)*16+(sp)
(2) (sp) = (sp)+2
CPU執行retf指令時,進行下面4步操作
(1) (IP) = (ss)*16+(sp)
(2) (sp) = (sp)+2
(3) (CS) = (ss)*16+(sp)
(4) (sp) = (sp)+2
可以看出,如果我們用匯編語法來解釋ret和retf指令,則
CPU執行ret指令時,相當于進行:
pop IP
CPU執行retf指令時,相當于進行:
pop IP
pop CS
所以我們就明白
read_it:
mov ax,es
test ax,#0x0fff
die: jne die ! es must be at 64kB boundary
xor bx,bx ! bx is starting address within segment
rp_read:
mov ax,es
cmp ax,#ENDSEG ! have we loaded all yet?
jb ok1_read
ret
當我們跳轉到read_it這里后,CPU會一直執行下去,直到ret,我們剛剛存儲的值就會出棧,就會回到原來的地方。
中斷
在我們開始說int指令的時候,我們先來說下中斷。
任何一個通用的CPU,比如8086,都具備一種能力,可以在執行完當前正在執行的指令之后,檢測到從CPU外部發送過來的或內部產生的一種特殊信息,并且可以立即對所接收到的信息進行處理。這種特殊的信息,我們可以稱其為:中斷信息。中斷的意思是指,CPU不再接著(剛執行完的指令)向下執行,而是轉去處理這個特殊信息。
那當CPU收到中斷信息后,應該轉去執行該中斷信息的處理程序。既然要執行那里的程序,就需要修改CS:IP指向它的入口(即程序第一條指令的地址)。那地址如何獲得呢?
中斷信息中包含著標識中斷源的類型碼。中斷類型碼的作用就是用來定位中斷程序處理程序。比如CPU根據中斷類型碼4,就可以找到4號中斷的處理程序。可隨之而來的問題是,若要定位中斷處理程序,需要知道它的段地址和偏移地址,而如何根據8位的中斷類型碼得到中斷處理程序的段地址和偏移地址呢?
通過中斷向量表找到相應的中斷處理程序的入口地址。
中斷向量表在內存中保存,其中存放著256個中斷源所對應的中斷處理程序入口。
對于8086CPU機,中斷向量表制定放在內存地址0處。從內存0000:0000到0000:03FF的1024個單元中存放著中斷向量表。
中斷處理,就是緊急處理,那處理完緊急的事,我們還是要回來原來的地方繼續執行下去。那,就是像我們剛剛call一樣,我們需要用到棧來保存我們現在的CS和IP。
大概說明中斷這個過程
(1)(從中斷信息中)取得中斷類型碼
(2)標志寄存器的值入棧(因為在中斷過程中要改變標志寄存器的值,所以先將其保存在棧中);
(3)設置標志寄存器的第8位TF和第9位IF的值為0(這一步的目的后面將介紹)
(4)CS的內容入棧
(5)IP的內容入棧
(6)從內存地址為中斷類型碼x4 和 中斷類型碼x4+2的兩個字單元中讀取中斷處理程序的入口地址設置IP和CS。
簡潔點就是
1. 獲得中斷類型碼N;
2. pushf
3. TF=0, IF=0
4. push CS
5. push IP
6. (IP) = (N * 4) , (CS) = ( N * 4 + 2)
既然我們把我們現在的CS和IP入棧了,可想而知,中斷處理程序一定會有一個指令返回。
即,iret,可以描述為
pop IP
pop CS
popf
標志寄存器,先不講
int指令
那現在再來說int 就簡單多了。
int指令的格式為:int n,n為中斷類型碼,它的功能是引發中斷過程。
CPU執行int n指令,相當于引發一個n號中斷的中斷過程,執行過程如下。
(1)取中斷類型碼n:
(2)標志寄存器入棧,IF=0,TF=0
(3)CS、IP入棧
(4) (IP)=(n*4),(CS)=(n*4+2)
從此處轉去執行n號中斷的中斷處理程序。