2007年10月31日 星期三

簡易用C語言求方程式的根

Standard
最近有不少人向我求救,原因不外乎是學校的報告、作業,而其中最令人覺得有趣的,就是關於數值方法的程式撰寫的問題。而講到用電腦處理數學運算,大多數人的直覺一定是使用 MATLAB 來實作其程式。是的,課堂上所教所講的就是 MATLAB,在處理各種數學問題和方程式,MATLAB 都有完整的函式指令和功能,只要熟悉,可以很輕易的能夠達成各種數學任務。不過最好笑的是,教授雖然教的是 MATLAB,但考的卻是 C 和 Fortran,要求學生的報告、作業也都是 C 和 Fortran,這讓一群學生都傻了眼,不知道該怎麼辦。

先暫且不管課本中是否附有 MATLAB 的程式碼,對於一群只上過幾門程式語言課的人,仍然無法輕易將程式轉用 C 或 Fortran 來寫。像是這樣一個用C語言求方程式的根的程式,就是許多人的問題:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <conio.h>

#define F(x) sin(10*x)+cos(3*x)

double f(double x) {
return F(x);
}

int sign(double x) {
if (x<0)
return -1;
else if (x>0)
return 1;
else
return 0;
}

void incsearch(double xmin, double xmax, int ns)
{
int i,n;
double space,current,ans;

current = xmin;
space = (xmax - xmin) / (ns-1);

for (i=0;i<ns-1;i++) {
if (sign(f(current))!=sign(f(current+space))) {
printf("%.4f\t%.4f\n", current, current+space);
}
current += space;
}
}

int main(int argc, char *argv[])
{
int n;

printf("Number of subintervals: ");
scanf("%d", &n);
incsearch(3, 6, n);
printf("Orz...");

getch();
return 0;
}



這程式是求方程式:『f(x)=sin(10x)+cos(3x)』,x 在 3 到 6 之間的所有根。原理是將 x 軸做 n 次切割,並一一代入方程式以檢查根所在的範圍,所以 n 設越大,相對的答案範圍越精準。這部份我寫了個函式 incsearch() 尋找根,其利用正負來判斷根是否存在某區間內。

另外,本程式中實作了 MATLAB 中的 sign(),用來檢查數值是大於、等於或小於零。

後記

事實上求根要求的精準,還有更多方法可以使用,本文講的只是一種簡易求根的方法,想要知道更多,可以參考數值分析處理方面的書籍。亦或是日後又有人求救相關問題,在我幫忙完後有空也會在這記錄存檔起來。

2007年10月22日 星期一

OrzMicrokernel tasks.asm 建立新工作函式註解

Standard
因為每天花在 Orz Microkernel 上的時間不多,而且這次的程式片斷比較長,所以讓我多花了幾天的時間做整理,直到現在才大約有個較完整的註解。繼續依順續閱讀 main.asm 中可以看到,在工作初始化多工機制和工作狀態的資料結構之後,就開始連續載入並新增 6 個工作到我們的工作清單中,main.asm 呼叫 tasks.asm 裡的 CreateTask 函式來新增工作。

以下是程式碼和詳細註解:
;--------------------------------------------------------------------------------------
; 建立新的工作 CreateTask
;;;; 輸入值: EAX = 工作的開始位址, EBX = 工作所佔的記憶體大小, ECX = 工作的堆疊大小
;;;; EDX = 工作的資料大小, ESI = 工作名稱指標
;;;; 返回值:若進位旗標 CF=1 則表示建立新工作失敗,否則 EAX = 工作編號
;;;; 會影響到的暫存器:EAX, EBX, ECX, EDX, ESI, EDI, EBP

CreateTask:
mov [taskNamePtr], esi ; 設 taskNamePtr 為工作的名稱
mov [taskBegin], eax ; 設 taskBegin 為工作的開始位址
mov [copySize], ebx ; 設 copySize 為工作所佔的記憶體大小
add ebx, ecx ; EBX 加工作的堆疊大小
add ebx, edx ; EBX 加工作的資料大小
mov [taskSize], ebx ; taskSize = 所佔記憶體 + 堆疊大小 + 資料大小

mov ebx, [tasksCount] ; 設定 EBX 為目前工作數量
cmp ebx, MAX_TASKS ; 比較工作數量是否已達最大值
jc CreateTask0

stc ; 設進位旗標 CF = 1
ret

CreateTask0:
mov esi, tasksList ; 工作清單的記憶體位址
add esi, ebx ; 位移 EBX

mov ebp, tasks ; 設定 EBP 為工作狀態記錄所在記憶體位址
xor cl, cl ; CL 歸零
xor edx, edx ; EDX 歸零

CreateTask1:
cmp [ebp + s_task.taskID],edx ; 比對工作編號
jz CreateTask2 ; 工作編號已經存在

add ebp, s_task.msize ; 移到下一個工作狀態記錄的記憶體開始處
inc cl ; CL 加一
jmp short CreateTask1 ; tasksCount < MAX_TASKS

CreateTask2:
; 初始化工作的各種暫存器、堆疊等狀態
mov [ebp + s_task.r_eip], edx ; 初始化指標暫存器﹝工作目前執行到的位置﹞
mov [ebp + s_task.r_eax], edx ; 初始化 EAX 累積暫存器
mov [ebp + s_task.r_ebx], edx ; 初始化 EBX 基底暫存器
mov [ebp + s_task.r_ecx], edx ; 初始化 ECX 計數暫存器
mov [ebp + s_task.r_edx], edx ; 初始化 EDX 資料暫存器
mov [ebp + s_task.r_esi], edx ; 初始化 ESI 來源索引暫存器
mov [ebp + s_task.r_edi], edx ; 初始化 EDI 目的索引暫存器
mov [ebp + s_task.r_ebp], edx ; 初始化 EBP 基底指標暫存器
mov [ebp + s_task.state], BYTE STATE_RUNNING ; 設定工作執行狀態

push esi ; 將 ESI 存入堆疊
push ecx ; 將 ECX 存入堆疊
mov eax, [taskSize] ; 設 EAX 為工作記憶體大小
call SizeToPages ; 計算工作所佔記憶體分頁數
mov [ebp + s_task.taskPages], ecx ; 將分頁數儲存至工作狀態記錄的資料結構中
push ebp ; 將 EBP 存入堆疊
call AllocPages ; 呼叫 mm.asm 的子程式初始化記憶體分頁
pop ebp ; 從堆疊取回工作狀態記錄所在記憶體位址
pop ecx ; 從堆疊取回工作的堆疊大小
pop esi ; 從堆疊取回工作清單的記憶體位址
jnc CreateTask3

stc ; 設進位旗標 CF=1
ret

CreateTask3:
mov [esi], cl ; 設定 ESI 為工作的堆疊大小
inc DWORD [tasksCount] ; 工作總數量加一

lea esi,[ebp + s_task.queue] ; 將 ESI 指向 s_task.queue
mov [ebp + s_task.qFirst], esi ; 將 s_task.qFirst 指向 ESI
mov [ebp + s_task.qLast], esi ; 將 s_task.qLast 指向 ESI

mov [ebp + s_task.taskPageAddr], eax ; 設 s_task.taskPageAddr 為工作記憶體分頁位址

mov eax, [ebp + s_task.taskPages] ; 設 EAX 為分頁數
imul eax, 0x1000 ; EAX = 分頁數 x 4096
dec eax ; EAX = EAX - 1
mov [ebp + s_task.r_esp], eax ; 將分頁數設定到工作的堆疊暫存器(從零開始算)

xor eax,eax ; 清除 ZF 零值旗標
clc ; 清除 CF 進位旗標
sti ; 設 IF 中斷旗標等於一
pushfd ; 將所有暫存器內容存入堆疊
cli ; 設 IF 中斷旗標等於零
pop eax ; 從堆疊中取回 EAX 值
; (*)
mov [ebp + s_task.r_eflags], eax

mov esi, [taskBegin] ; 設 ESI 為工作的開始位址
mov edi, [ebp + s_task.taskPageAddr] ; 設 EDI 為工作記憶體分頁位址
mov ecx, [copySize] ; 設 ECX 為工作所佔的記憶體大小
rep movsb ; 將工作的資料從 ESI 複製到 EDI 工作記憶體分頁位址

lea edi, [ebp + s_task.taskName] ; 將 EDI 指向 s_task.taskName
mov esi, [taskNamePtr] ; 設 ESI 為工作名稱(位於 taskNamePtr 的內容)
mov ecx, MAX_TASK_NAME ; 設 ECX 為工作名稱的最大字數

; 將工作名稱從 ESI(taskNamePtr) 複製到 EDI(s_task.taskName)
CreateTask4:
lodsb ; 從 ESI 複製 1 Byte 到 EAX(AL)
and al, al ; 判斷此 Byte 是否為空值
jz CreateTask5 ; 如果 AL 為零則跳到 CreateTask5
stosb ; 如果 AL 有值則從 EAX 複製 1 Byte 到 EDI(s_task.taskName)
loop CreateTask4 ; 重覆執行複製直到 AL 為空值
xor al, al ; 若工作名稱超過最大字數,設零值旗標 ZF=1 、同位旗標 PF=1
CreateTask5:
stosb ; 從 EAX 複製 1 Byte 空字元『\0』到 EDI(s_task.taskName)

mov eax, [currentID] ; 設 EAX 為目前工作編號
mov [ebp + s_task.taskID], eax ; 設 s_task.taskID 為 EAX
inc eax ; 目前工作編號加一
; EAX!=0 PID
mov [currentID],eax ; 設定下一個工作編號

clc ; 清除 CF 進位旗標
ret

taskBegin dd 0x12345678
taskSize dd 0x12345678
copySize dd 0x12345678
taskNamePtr dd 0x12345678


後記

tasks.asm 的程式比較長,牽涉到的資料結構也比較雜,但這是多工作業系統的命脈。另外一提,在 CreateTask3 裡從 lea esi,[ebp + s_task.queue] 開始的三行程式碼,應該要有更詳細的說明,但這次主題是在建立工作,所以其中牽涉到更多完整工作佇列運作的內容,就暫且不多加解釋,待日後有機會再回頭重新詳解。

2007年10月19日 星期五

Gmail 輸人不輸陣,免費空間快速爆增

Standard
這幾天瑣事繁多,令我閒不得半刻,所以有空的時間最多只能上網看看信件、檢查有無回應文章。不過就在這兩天登入信箱的時候,我發現 Google 開始有非常大的動作,那就是空間爆增了!截至這篇文章為止,我的信箱可用空間已達 3.7 GB!

回憶過去我選擇了 Gmail 來當我的私人信箱,就是因為它在操作介面上設計簡單輕便、且空間大﹝在 Gmail 剛推出 2GB 時其他免費信箱還只有5MB、10MB﹞。可是到了今天這時候,Hotmail 已經開始擴充到 5GB 的免費空間,Yahoo 也不斷有擴大空間的改進,令 Gmail 當初的空間已經不夠看了!可想而知, Gmail 的大容量吸引力就這樣開始慢慢消退。

但是,這幾天發現,我的信箱容量開始以每日約 200 MB 的速度增加,從2.9GB、3.1GB、3.3GB一直到今天的3.7GB,似乎還有繼續快速往上發展的趨勢,真是期待它會增加到什麼樣的地步。看來 Gmail 輸人不輸陣,似乎打算跟其他家服務商拼了。 :)

2007年10月15日 星期一

OrzMicrokernel tasks.asm 初始化多工機制和工作狀態的資料結構

Standard
main.asm 在設定完顯示模式後,緊接著就是開始初始化多工機制。我們將配置一塊記憶體來存放所有工作的名稱、執行狀態﹝包括各種暫存器的內容﹞,在 Orz Microkernel 中,工作數量被限制為 64 個,它被定義在 define.asm 裡頭。

程式碼和詳細註解:
InitTasks:
mov ecx, MAX_TASKS ; 設 ECX 為最大工作數量﹝定義在 define.asm﹞
mov ebp, tasks ; 設定工作狀態記錄所在位址﹝定義在tables.asm﹞
xor eax, eax ; 將 EAX 歸零

InitTasks0:
mov [ebp + s_task.taskID], eax ; 初始化工作狀態記錄編號
add ebp, s_task.msize ; 移到下一個工作狀態記錄的開頭
loop InitTasks0 ; 重覆工作狀態記錄初始化 ecx 次
ret



從上面程式碼看到的 s_task,是工作狀態記錄的資料結構,它被定義在 task.asm 的開頭:
; 使用虛擬指令 RESB、RESD 宣告不含初值的資料結構 s_task
struc s_task
.taskID: resd 1 ; 工作編號 Dword(2 Bytes)
.state: resb 1 ; 工作狀態 Dword(1 Byte)
.newTick: resd 1 ; need for state=STATE_SLEEP

.queue: resd (MAX_TASKS + 1)
.qFirst: resd 1
.qLast: resd 1

.s_taskID: resd 1
.s_message: resd 1
.s_dataPtr: resd 1
.s_dataSize: resd 1

.taskPageAddr: resd 1
.taskPages: resd 1

; 工作執行時的各項暫存器設定
.r_eflags: resd 1
.r_eax: resd 1 ; EAX 累積暫存器
.r_ebx: resd 1 ; EBX 基底暫存器
.r_ecx: resd 1 ; ECX 計數暫存器
.r_edx: resd 1 ; EDX 資料暫存器
.r_esi: resd 1 ; ESI 來源索引暫存器
.r_edi: resd 1 ; EDI 目的索引暫存器
.r_ebp: resd 1 ; EBP 基底指標暫存器
.r_esp: resd 1 ; ESP 堆疊指標暫存器
.r_eip: resd 1 ; EIP 指標暫存器

.taskName: resb (MAX_TASK_NAME + 1) ; 工作名稱

.msize:
endstruc ; 資料結構結尾



後記

在資料結構的中間一段我尚未註解,因為我偷懶 :)
但我卻厚臉皮的還是要強辯,因為那部份牽涉到日後工作的執行,所以之後再補上註解就好了,現在只是初始化資料會用到的記憶體,暫時不用理解到這麼多。

我現在好像都是在夜深人靜的時候,才來註解一下 Orz Microkernel。不過說真的,這工作當睡前腦部運動真是不錯的選擇﹝笑﹞。

2007年10月14日 星期日

OrzMicrokernel video.asm 切換顯示模式的註解

Standard
依照 Orz Microkernel 在 main.asm 的核心啟動流程來看,在轉換到保護模式並做了最基本的初始化之後,就會 call SetVideoMode 來切換顯示模式到 320x200。關於這部份,比較複雜的是牽涉到 BIOS 中斷,由於在保護模式之下無法使用 BIOS INT,所以特別額外在 realmode.asm 寫了在保護模式下處理此類中斷的呼叫函式﹝關於 realmode.asm 的註解日後補上﹞。

程式碼和詳細註解:
;------------------------------------------------------------------------------
; 文字模式﹝80x25,16色﹞:AL=0
; 圖形模式﹝320x200,256色﹞:AL=1

SetVideoMode:
and al, al
jnz SetVideoMode0 ; 若 AL 不為 0 則呼叫 SetVideoMode0

mov [rmode_ax], WORD 0x03 ; 文字模式
mov al, 0x10 ; 中斷 10H

; 在保護模式下舊有的中斷向量表已被 IDT 代替
; 導致 BIOS 中斷在保護模式下無法使用
; 故 BIOS 中斷得在真實模式執行
; 因此特別在 realmode.asm 中設計一個處理真實模式中斷的函式
jmp RmodeInt ; 呼叫在 realmode.asm 的 RmodeInt 處理中斷

SetVideoMode0:
dec al ; AL=AL-1
jnz SetVideoMode1 ; 若 AL 不為 0 則呼叫 SetVideoMode1 返回

mov [rmode_ax], WORD 0x13 ; 圖形模式
mov al, 0x10 ; 中斷 10H
jmp RmodeInt ; 呼叫在 realmode.asm 的 RmodeInt 處理中斷

SetVideoMode1:
ret



後記

從現在開始,我們將會慢慢感受到 Protect Mode 的複雜度,這也是作業系統核心設計最無聊煩悶的起步 :)

2007年10月13日 星期六

OrzMicrokernel bit32.asm 進入保護模式後的核心初始化

Standard
OrzMicrokernel 在 bit16.asm 完成了轉換成保護模式的工作,緊接著會跳到 bit32.asm 初始化核心和設定硬體中斷周期。最重要的是,從跳轉到這檔案後,就可以開始使用 32 位元的各種延伸暫存器以及更大的記憶體定址,但前題是:我們得先將新的暫存器區段位址設定好,因為暫存器設定還是處於在真實模式時的狀態。

程式碼和詳細註解:
[BITS 32]
; Intel 處理器的設計是以 dword 為單位來存取記憶體,所以記憶體位址定為 4 的倍數。
align 4 ; 設定所有資料的起始位址都是 4 的倍數
Begin32c:
; 初始化暫存器區段
mov ax, kernel_data - _GDT ; 從 tables.asm 的 kernel_data 開始
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov esp, 0x2FFFF ; 堆疊暫存器設在 2MB 記憶體後

call TestMemory ; 呼叫 mm.asm 的子程式測試記憶體

mov edi, memMap ; 初始化索引暫存器
;
memMap 記憶體大小有0x20000=128KB
mov ecx, 0x100 / 32 ; 計數器設為 256/32=8
xor eax, eax ; 將 eax 歸零
dec eax
rep stosd ; 每次從 EAX 中複製 4 Bytes 到 EDI
; 複製 8 次


mov ecx, 0x20000 / 4 - (0x100 / 32) ; 計數器設為 32760
xor eax,eax ; 將 eax 歸零
rep stosd ; 每次從 EAX 中複製 4 Bytes 到 EDI
; 複製 32760 次


; 設定 825x 計時器
; 模式控制暫存器(I/O 埠編號 43H):共 8 位元
;
; 位元 位元值 說明
; 0 0 使用二進位格式的數值
; 1 使用 BCD 格式的數值
; 1,2,3 000 計時模式 0
; 001 計時模式 1
; 010 計時模式 2
; 011 計時模式 3
; 100 計時模式 4
; 101 計時模式 5
; 4,5 00 保留住目前數值
; 01 僅讀寫較高的位元組
; 10 僅讀寫較低的位元組
; 11 先讀寫較低的位元組,再讀寫較高的位元組
; 7,6 00 選擇計時通道 0
; 01 選擇計時通道 1
; 10 選擇計時通道 2
; 11 指定讀回命令
mov al, 0x34 ; 設定計時器
; 設定值為 00110100:
; 使用二進位格式的數值
; 計時模式 2
; 先讀寫較低的位元組,再讀寫較高的位元組
; 選擇計時通道 0
out 0x43, al ; 設定 AL 至模式控制暫存器

; 設定時間周期為 10ms(100Hz)
; 即每 10ms 產生一次中斷信號
; 設定值算法:1193180 / 100 = 11931 = 0x2e9b
; 石英震盪器每秒震盪 1193180 次
; 頻率 100Hz
mov al, 0x9b ; 先設定低位元 0x9b
out 0x40, al ; 輸入到計時器0
mov al, 0x2e ; 再設定高位元 0x2e
out 0x40, al ; 輸入到計時器0

call EnableIRQs ; 呼叫 irqs.asm 的子程式以啟動 IRQ 機制



後記

這次比較特別的部份,就是牽扯到 Timer 的操作,我想應該只有少數的 Programer 有機會直接接觸到 825x 計時器,我們過去還得靠它才能讓 PC Speaker 發出難聽的電子旋律呢!(笑)

2007年10月11日 星期四

OrzMicrokernel bit16.asm 核心前置動作並轉換到保護模式的註解

Standard
bit16.asm 是 OrzMicrokernel 從真實模式轉換到保護模式的部份。保護模式是作業系統必要的運作環境,因為保護模式支援了 4GB 的實體記憶體定址、可擴充的記憶體分頁機制、多重任務機制、特權級等多工作業系統會用到的各種機制。

詳細程式碼和註解:
[BITS 16]
[ORG KERNEL_START]

Start:
; 初始化暫存器區段
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax ; 初始化堆疊區段
mov sp, 0x0000 ; 堆疊指標暫存器歸零

; 準備開始切換到保護模式
cli ; 禁用中斷以免干擾初始化
cld ; 設定字串處理時由低住址往高位址
call PrepareIRQs ; 呼叫 irqs.asm 的副程式以初始化 IRQ

; A20 位址線控制器:
; 早期 8086 的 CPU 只有 20 bits 的位址匯流排,只能定址到 1MB 的記憶體
; 因此,如果試圖存取超過 1MB 的記憶體,將會回到位址零重新定位循環下去
; 所以,要打開 A20 位址線,讓電腦可以定址大於 1MB 的記憶體
call A20En ; 呼叫 irqs.asm 的副程式
; 以打開 A20 位址線

lgdt [_GDT - KERNEL_START] ; 載入 GDT:_GDT 在 tables.asm
lidt [_IDTR - KERNEL_START] ; 載入 IDT:_IDTR 在 tables.asm

; 切換到保護模式
; 切換成保護模式的方法:將 CR0 的 PE 欄位(Bit0)設為 1
;
; 控制暫存器 CR0 的結構:
; 位元編號 [31][30][29][28-19][18][ 17 ][16][15-6][05][04][03][02][01][00]
; 欄位名稱 [PG][CD][NW][保 留][AM][保 留][WP][保 留][NE][ET][TS][EM][MP][PE]

mov eax, cr0 ; 將 cr0 內容放入 eax 暫存器
; 設定 CR0 的 PE(Bit0) = '1'
or eax, 00000000000000000000000000000001b
and eax, 10011111111110101111111111111111b
mov cr0, eax ; 將 eax 回存設定至控制暫存器

; 將 kernel_code 程式段放入 CS 程式區段
; 然後立即切換到保護模式並執行在32bit.asm
; 的 Begin32c
jmp DWORD (kernel_code - _GDT):Begin32c


後記

boot.asm 還有一部份尋找系統核心的程式還未註解完成,我會在最近慢慢將它完成。不過講再多的 boot.asm,OrzMicrokernel 的作業系統核心還是沒有被啟動,未免令人感到煩悶,這篇換換口味來談談載入核心後,的核心初始化和保護模式轉換。

2007年10月10日 星期三

OrzMicrokernel boot.asm 啟動磁區定義和程式初始化註解

Standard
OrzMicrokernel 的 boot.asm 是針對 1.44 MB 磁碟片所設計的,採用的是 FAT12 ﹝注意不是 FAT16 或 FAT32﹞的檔案系統,所以,在啟動磁區的部份的定義上比較簡單且單純。另外,這次註解的內容主要是程式一開始的部份,包括初始化、在螢幕上顯示一段文字等。

程式碼與詳細註解:
Base:
jmp short Begin ; 從 Begin 執行
nop ; 空執行

; 啟動磁區格式
oem db 'OrzMicro' ; 廠商軟體名
bytesPerSector dw 0x0200 ; 設每磁區的大小為 512 Bytes
sectPerCluster db 0x02 ; 每磁叢的磁區數
resSectors dw 0x01 ; FAT 前的磁區數
; ﹝開機記錄所佔用的磁區數﹞
fatCopies db 0x02 ; FAT 表總數
rootEntCnt dw 0x0070 ; 根目錄的最大檔案數
totalSectors dw 0x05A0 ; 邏輯磁區總數
media db 0xF9 ; 磁碟種類
sectPerFat dw 0x0003 ; 每個 FAT 表的磁區數
sectPerTrack dw 0x0009 ; 每磁軌的磁區數
heads dw 0x0002 ; 磁頭數﹝面數﹞
hiddenSectors dd 0 ; 隱藏磁區數
sectorHuge dd 0 ; 保留 4 Bytes
drive db 0 ; 中斷 INT13 的磁碟機編號
db 0 ; 保留 1 Byte
extendedBPB db 0x29 ; 開機特徵碼
volumeID dd 0 ; 磁碟序號
volumeLabel db 'Orz Disk ' ; 磁碟標籤(11 Bytes)
fileSys db 'FAT12 ' ; 檔案系統類型﹝必須為 8 Bytes﹞

;------------------------------------------------------------------------

Begin:
; 初始化程式
cli ; 禁用中斷以免干擾初始化
cld ; 設定字串處理時由低住址往高位址
mov ax,cs ; 初始化暫存器
mov ds,ax
mov ss,ax
mov sp,7c00h ; 此程式將在 0000:7c00 執行
sti ; 啟用中斷

mov [curDrive],dl ; 從 BIOS 取得目前使用的裝置

;-----------------------------------------------

; 初始化顯示記憶體並顯示核心訊息
; 函式 Print 的參數:SI = 字串, ES:DI = 顯示記憶體位置, AH = 顏色
mov ax,$03 ; 為中斷設定 AH=03h 以讀取游標位置
int $10 ; 開始中斷 10h

; 設定顯示記憶體位址到 ES:DI
mov ax,$0B800 ; 設定顯示記憶體位址:0xB8000
mov es,ax ; 設定 ES 區段
xor di,di ; 設定 DI:由第零列第零行開始寫入

; 在螢幕上顯示核心訊息
mov si,fname ; 設定 SI:字串為核心檔案名稱
mov ah,$2F ; 設定 AH:字串顏色為白色
call Print ; 呼叫顯示字串函式



而關於螢幕顯示字串的部份,被拉出去做成了一個副程式 Print 方便日後經常使用:

; 顯示字串函式
; SI = 字串, ES:DI = 顯示記憶體位置, AH = 顏色
Print:
mov al,[cs:si] ; 取得 SI 裡的字串
and al,al ; 判斷如果為字串結尾
jz Print0 ; 則顯示字串函式結束返回

mov [es:di],al ; 將字串第一個位置的字元輸出到顯示記憶體
inc di ; 顯示記憶體位置往後移動一位以設定顏色
mov [es:di],ah ; 設定該字元顏色

inc di ; 顯示記憶體位置往後移動一位
inc si ; 設定指向下一個字元
jmp short Print ; 呼叫函式顯示下一個字元
Print0: ret



後記

由於在 blog 上不能顯示 <TAB> ,所以在貼程式碼的時候都要手動將 <TAB> 換成空格,這應該算是最累人的部份。而且一行程式碼或註解如果太長,就會超出顯示範圍消失在邊界之外,讓人要一直不停去檢查,手動做換行的動作。唉,在 Blog 貼程式碼真是一件辛苦的事﹝汗﹞。

2007年10月9日 星期二

OrzMicrokernel boot.asm 磁碟內容參數的詳細註解

Standard
這是 OrzMicrokernel 裡 boot.asm 磁碟內容參數的部分,這部份比較直觀且不涉及硬體裝置的操作和定義,只要了解磁碟的資料配置結構,很容易就能了解其中的運作。

程式碼包括詳細註解:
;-----------------------------------------------
; 計算各項磁碟內容參數
;
; 計算磁區位置:
; 1. FAT表磁區
; 2. 根目錄磁區
; 3. 資料磁區
;
; 計算 FAT 檔案系統資訊:
; 1. FBD 所佔磁區數量
;-----------------------------------------------
;
; 磁碟資料結構:[開機記錄磁區][隱藏磁區][FAT表磁區][根目錄磁區][資料磁區]
; [FAT表磁區] 是由許多 FAT 表所構成
; [根目錄磁區] 是由許多 FBD 所構成
; FBD(檔案描述區塊):大小為 32Bytes
; 每個檔案都有自己的 FBD,記錄檔案名稱、建檔日期、檔案大小等資料

; 計算 FAT 的開始位置
; 方法:跳過隱藏磁區和開機磁區
; hiddenSectors = [高位元 2Bytes][低位元 2Bytes]
mov si,[hiddenSectors] ; 低位元給 SI
mov di,[hiddenSectors+2] ; 高位元給 DI
add si,[resSectors] ; 跳過隱藏磁區加開機記錄所佔用的磁區數
adc di,0 ; DI:SI = 第一個 FAT 表的開始位置
mov [fatStart],si ; 用 fatStart 保存 FAT 的開始位置
mov [fatStart+2],di

; 計算根目錄磁區的開始位置
; 方法:跳過 FAT 表磁區
; FAT 表的總數 x 每個表的磁區數 = 所有 FAT 表的磁區總數
mov al,[fatCopies] ; AL = FAT 表總數
xor ah,ah ; AX 暫存器中我們只用到其中的 AL,故要將 AH 歸零
mul WORD [sectPerFat] ; AL 乘以每個表的磁區數:DX:AX = 所有 FAT 表所
; 佔的磁區總數

add si,ax ; 跳過 FAT 表的磁區總數
adc di,dx ; DI:SI = 根目錄磁區的開始位置
mov [rootStart],si ; 用 rootStart 保存根目錄磁區的開始位置
mov [rootStart+2],di


; 計算全部 FBD 所佔磁區數
; 方法:根目錄最大檔案數 x 單一FBD大小(32Bytes) = 根目錄全部 FBD 所佔空間
mov ax,[rootEntCnt] ; 取得根目錄的最大檔案數
mov cl,5 ; TODO: reminding yet?
shl ax,5 ; 將 AX 向左平移 5 bit(最大檔案數乘以32) = 全部
; FBD 所佔大小

xor dx,dx ; 清除 DX,因為下一行 DIV 指令會用到 DX:AX
div WORD [bytesPerSector] ; 全部 FBD 所佔大小除以磁區大小 = 根目錄全部 FBD
; 所佔磁區數

mov [secPerRoot],ax ; 用 rootStart 保存根目錄全部 FBD 所佔磁區數

; 計算資料磁區的開始位置
add si,ax ; 跳過根目錄磁區(即跳過FBD 所佔磁區數)
adc di,0 ; DI:SI = 第一個資料磁區位置
mov [dataStart],si ; 用 dataStart 保存第一個資料磁區的位置
mov [dataStart+2],di



後記

這一段程式,唯一比較令我有些疑問的是 mov cl,5 這行,shl 有時會需要用到 cl 來設定平移的 bit 數這我能理解,可是在它下一行的 shl 是直接指定數值 5 當平移量,並非以 shl ax,cl 來做平移操作,那麼,設定 cl=5 是有必要的嗎?還希望有知道的人能給我一些回答。

2007年10月8日 星期一

OrzMicrokernel 陪我度過颱風天的阿宅生活

Standard
最近幾次少數能以『強烈的颱風』身份影響並登陸台灣的,大概就屬這次的颱風了。還沒登陸,風就已經重擊了家裡的窗戶,呼呼砰砰聲搞得我醒著做事覺得煩、睡也睡不好。前兩天晚上去逛敦南誠品,在去的路上,風狂野的在短短一個街口,就把我的雨傘給吹成奇形怪狀﹝比翻過來還可怕﹞,後來回家時只好淋著雨一身濕答答的走回停車處。在被風玩弄之後,我就決定這兩天不出門了,弄得自己閒得發荒,一直在家當阿宅 Orz。

我突然想起前陣子 Jserv 寫的 OrzMicrokernel,一直沒能好好的找個時間去讀它,所以,藉著這次颱風過境,就好好品嘗一下了﹝笑﹞。一切果然如同大家所說, OrzMicrokernel 真的是簡潔有力,是一個作業系統設計的優良範例,它的設計非常直觀易懂;可是我也有聽到一群朋友,看得要死還是看不大懂 OrzMicrokernel 。搞半天,原來他們對組合語言並不是說非常熟悉,還有對磁碟 LBA、CHS、FAT12 等等這種格式定義很陌生!不過也難怪,大多人平時寫程式都太高階了,早就忘了電腦是怎麼動起來的。

尋思如果很多想了解的人,都卡在對組合語言的一點不熟悉和資料結構定義這種問題上,導致無法踏入 OrzMicrokernel 的話,那真的是太可惜了。所以,無聊到極點的我,就決定開始幫 OrzMicrokernel 逐行中文註解,希望從組合語言指令用法開始加註說明,還有說明重要的資料結構﹝註解寫起來比程式碼還多行,驚!﹞。為了說明的完全清晰正確,我還重新查資料驗證自己的觀念,生怕有一點遺漏。

以下是 boot.asm 讀取磁區的註解片段:

; DX:AX = LBA 磁區位置, ES:BX = 緩衝區位址
ReadSector:
; 磁區 CHS 表示法
; 磁碟和磁片的記錄方式『不是』一整面的磁區用完再換面儲存,而是以空心圓柱狀的方式。
; 因為磁片只有簡單兩面,以磁片為例:
; 正確的磁片資料記錄順序為:第零面第零軌、第一面第零軌、第零面第一軌、第一面第一軌...
; 每面的同一軌都用滿後才換下一個磁軌紀錄資料,形狀如同空心圓柱,故稱磁柱(cylinder)。
; 因此以磁片來說,第 0 面第 0 軌和第 1 面第 0 軌就是第 0 磁柱

; 磁區 LBA 表示法
; LBA 編號法是不考慮磁區的實體 CHS 上,純粹將所有磁區統一由 0, 1, 2...編號到最後一個磁區

; 以 LBA 表示法換算成 CHS (磁柱、磁頭、磁區) 的表示法
; 計算 CHS 的磁區編號
div WORD [sectPerTrack] ; LBA 磁區位置除以每磁軌的磁區數 = 磁區在第幾磁軌 = AX
inc dl ; 上一行除法運算的餘數(DL)加一 = 磁區編號
mov cl, dl ; 設 CL 為磁區編號

; 計算 CHS 磁頭編號
xor dx,dx ; 清除歸零 DX
div WORD [heads] ; 磁軌數量(AX)除以磁頭數﹝面數﹞
mov dh,dl ; 設 DH = 磁頭編號(餘數)

mov ch,al ; 設 CH = 磁柱
mov dl,[curDrive] ; DL = 磁碟裝置

mov ax,$0201 ; AH = 02H 讀取指定的磁碟資料、讀取的磁區數目 AL = 1
int $13 ; 磁碟的 BIOS 服務中斷,ES:BX 會被設為存放資料的記憶體指標
; 如果錯誤會設旗標 CF=1
jc PrintError ; 如果 CF=1 呼叫 PrintError 函式顯示錯誤
ret


後記

以上只是其中一小段落,其餘得我大概還要花很大工夫才能完成,因為每一句註解我都重複修改了很多次以求完美,所以很費工時。或許有很多人覺得我的註解非常囉嗦甚至是脫褲子放屁,但為了考慮到各種程度的人都看得懂,可能沒辦法不這樣做了。另外,如果以上註解有誤,還請大家不吝賜教。謝謝。

2007年10月5日 星期五

實作支援多處理器(SMP)操作的程式

Standard
其實多核心早已不是新技術,還記得多年前,多處理器架構(SMP)早就廣泛應用在伺服器的硬體上,只是要在頂級的硬體規格才看得到。當時,伺服器專用的 CPU 實在是貴得嚇死人,頂級的伺服器配備兩顆以上的 CPU 後,更是價格喊到天價,就如同今日的 CPU 有『多核心』加持一般,在價格上的氣勢也因此莫名的居高不下。但是,我們真的有用到多處理器﹝多核心﹞架構的好處嗎?就連掌控 CPU 運算的多數程式設計人員大概從不曾想過這個問題,更不用說一般無知的消費者了。

以下是一個指定 CPU 來處理運算的例子:

#include <stdio.h>
#include <stdlib.h>
#include <sched.h>

int main(void) {
cpu_set_t cmask;
unsigned long len = sizeof(cmask);
__CPU_ZERO(&cmask); /* 初始化 cmask */
__CPU_SET(0, &cmask); /* 指定第一個處理器 */

/* 設定自己由指定的處理器執行 */
if (!sched_setaffinity(0, len, &cmask)) {
printf("Could not set cpu affinity for current process.\n");
exit(1);
}

return 0;
}


關鍵的註解

  • 我們使用 __CPU_SET() 指定處理器,你可以指定 0, 1, 2, 3... 等編號來決定使用哪一顆處理器。
  • sched_setaffinity() 是用來將程序轉移到我們所指定的處理器執行,你可以設定程序編號(PID)來選擇要轉移的程序,或是設定 PID 為 0,也就是自己。


後記

指定 CPU 處理運算在實際應用面上,可以將需要大量運算的工作分散給所有的 CPU 同時處理,或是也可以,控制某些特定功能的程式盡量利用同一顆 CPU 的 Cache ,避免資料分散,以增加 Cache 存取的速度。

備註:本文所講的內容是架構在 Linux 2.6 版核心之上的系統。

2007年10月1日 星期一

台灣大哥大的3G就是比較大!

Standard
如果有人看過我前一陣子發表的『遠傳 3.5G 山頂洞人專用的大頻寬』,一定知道我把遠傳的大寬頻罵個臭頭,但是,我對3G服務並不死心,我不相信每家業者都跟遠傳一樣如此無恥的欺負消費者,用詐騙的方式取得用戶。所以,這次我在一些因緣際會之下,換了台灣大哥大的3G服務。

不可否認的,台灣大哥大的3G頻寬就是比較大!同樣在板橋江子翠地區,不管任何時候,台灣大哥大都能保持在每秒100KB以上的速度。比起遠傳的速度5、7KB,相差十幾倍不止。在此,我們先不考慮3G訊號覆蓋率,就寬頻大小來說,台灣大哥大比遠傳好太多太多了。

雖然我在這稱讚台灣大哥大的3G比較優質,但是,還是未達到他們號稱3.6Mb的一半,也就是每秒300KB到400KB的一半,在此還是有待加強的。而且,兩家業者比較起來,或許是因為台灣大哥大的3G用戶比較少,所以才有比較大的頻寬也說不定。也就是,求大家不要來搶我的頻寬,等到他們把頻寬備齊完整,你們再來用吧!到時候我一定會公告給大家知道,絕不會一人獨享!