2010年12月28日 星期二

解決"sudo: must be setuid root"的問題

今天在自己的Linux桌機 (Ubuntu 10.10) 幹了一件傻事:
$ sudo chown -R adrian.adrian /usr/
結果導致使用sudo會有底下的問題:
$ sudo su
sudo: must be setuid root
這下可high了, Ubuntu在安裝的時候並沒有讓管理者設定root密碼, 所以無法使用root登入, 沒有root權限什麼事都不能做啊! 底下解決方式:

  1. 重新啟動Ubuntu,在grub選單中選擇有recovery mode的選項, Ex: "Ubuntu, with Linux 2.6.35-23-server (recovery mode)"
  2. 以recovery模式開機後, 會出現"Recovery Menu"選單, 選擇"root Drop to root shell prompt"選項就可進入root shell prompt。
  3. 利用ls -l觀看sudo資訊,所以是擁有此程式使用者的問題。
    -rwxr-xr-x 2 adrian adrian 147872 2010-09-01 04:39 /usr/bin/sudo
  4. 執行底下指令便可大功告成:
    # chwon root:root /usr/bin/sudo
    # chmod 4755 /usr/bin/sudo
    # reboot
  5. 最後還是乖乖地把/usr/重新設定回root。
    $ sudo chown root.root /usr/

【Reference】
[1] Ubuntu forum

2010年12月21日 星期二

[打造簡易作業系統 - 以GNU Assembler組合語言撰寫] (七) 利用Call Gate與TSS (Task-State Segment)實現特權等級的轉換

上篇文章僅介紹如何利用Call Gate,文中並未提及如何實現特權等級的轉換,也就是從低特權等級進入高特權等級,就如同Linux的user space (Privilege 3或稱Ring 3)進入kernel space (Privilege 0或稱Ring 0)。

簡介TSS (Task-State Segment)
本文僅簡單說明TSS在本文被使用的地方,如欲詳盡說明請參考[2]之第七章。

Task Structure
Figure 1為一個工作任務 (Task)的結構圖,一個工作任務分為兩大部份: 1. 工作任務的執行空間 (Task Execution Space),其中包含CS、SS以及若干個DS,也就是Figure 1右上角的三個區段。2. TSS: 由工作任務執行空間與一個儲存空間 (用來儲存工作任務的狀態)所組成。Figure 1又說明另一件事: TSS的Segment Selector必須儲存在Task Register。因此在使用TSS之前,必須使用LTR指令將TSS之Segment Selector載入至Task Register。

Figure 1 Structure of a Task

TSS Memory Layout

Figure 2為32位元TSS記憶體空間配置圖,如下所述:
  • ESP0、SS0、ESP1、SS1、ESP2與SS2: 分別為特權等級0、1與2的堆疊區段與堆疊指標,也就是Figure 1右下角三個 "Stack Seg. Priv. Level x"。本文利用ESP0與SS0實現特權等級的轉換 (Ring 3轉換至Ring 0),因此作者僅設定TSS的這兩個欄位,其它欄位則設為0。
  • 其它欄位在此不進一步探討,有興趣的網友可以參考[2]。

Figure 2 TSS Memory Layout

原始碼下載
由於篇幅的關係,往後該系列文章將不張貼程式碼,將提供連結下載方式,原始碼下載點


作業系統程式碼說明
Figure 3為作業系統程式碼解說圖,此圖說明GDT與LDT的記憶體配置圖與程式流程圖,其流程圖並非以傳統方式表示。取而代之,改以紅色圈圈的數字代表程式流程並搭配GDT與LDT記憶體配置圖,如此更能清楚地明白程式執行流程。底下將針對每一步驟 (紅色圈圈的數字)做詳盡的解釋:

Figure 3 High Level Perspective of OS code

  1. 使用ljmp指令由真實模式轉換至32位元保護模式 (Ring 0)。其片段程式碼如下所示:
    
      /* Jump to protected-mode OS code        *
       * ljmpl prototype:                      *
       *   ljmpl segment_selector_of_CS offset */
      ljmpl     $SegSelectorCode32, $0

    此區段程式碼將Msg1輸出至螢幕,並將LDT的segment selector載入至LDTR,接著使用lcall指令跳至LDT的CS,如下所示:
        /* Load LDT selector */
       mov     $(SegSelectorLDT), %ax
    
       /* Load LDT selector in GDT to LDT register */
       lldt     %ax
    
       /* Jump to code segment in LDT */
       lcall     $(SegSelectorLDTCode32), $0
    
  2. 如第1點所述。
  3. 如第1點所述。
  4. 輸出Msg2,隨即使用lret指令返回LABEL_GDT_CODE。
  5. 通常,call與ret指令必須配合使用。執行call指令時,處理器會將SS、ESP、CS與EIP推入(Push) 該執行空間的堆疊 (Stack)。接著,執行ret指令時,處理器會將EIP、CS、ESP與SS從堆疊取出 (Pop)。為了實現從Ring 0返回Ring 3,底下程式碼模擬處理器的工作: 將Ring 3的SS、ESP、CS與EIP推入(Push) 該執行空間的堆疊 (Stack)並使用lret指令返回Ring 3。
       pushl    $(SegSelectorStackR3)
      pushl    $(TopOfStackR3)
      pushl    $(SegSelectorCodeR3)
      pushl    $0
      lret
    
  6. 從Ring 0 (LABEL_GDT_CODE)返回 Ring 3 (LABEL_GDT_CODE_R3)。
  7. 此Ring 3程式區段將Msg1_R3輸出至螢幕,利用call gate並搭配TSS成功地返回Ring 0程式區段。因此,必須設定TSS之ESP0與SS0欄位,如下所示:
    LABEL_TSS:
     .4byte  0                         /* Previous Task Link   */
     .4byte  TopOfStackR0              /* ESP0                 */
     .4byte  SegSelectorStackR0        /* SS0                  */
     .4byte  0                         /* ESP1                 */
     .4byte  0                         /* SS1                  */
     .4byte  0                         /* ESP2                 */
     .4byte  0                         /* SS2                  */
     .4byte  0                         /* CR3 (PDBR)           */
     .4byte  0                         /* EIP                  */
     .4byte  0                         /* EFLAGS               */
     .4byte  0                         /* EAX                  */
     .4byte  0                         /* ECX                  */
     .4byte  0                         /* EDX                  */
     .4byte  0                         /* EBX                  */
     .4byte  0                         /* ESP                  */
     .4byte  0                         /* EBP                  */
     .4byte  0                         /* ESI                  */
     .4byte  0                         /* EDI                  */
     .4byte  0                         /* ES                   */
     .4byte  0                         /* CS                   */
     .4byte  0                         /* SS                   */
     .4byte  0                         /* DS                   */
     .4byte  0                         /* FS                   */
     .4byte  0                         /* GS                   */
     .4byte  0                         /* LDT Segment Selector */
     .2byte  0                         /* Reserved             */
     .2byte  (. - LABEL_TSS + 2)       /* I/O Map Base Address */
    .set TSSLen, (. - LABEL_TSS)
    
    在此,特別對特權等級轉換做進一步解釋,以便讓讀者了解為什麼只要設定ESP0與SS0欄位即可。當存取目的程式區段 (Destination Code Segment)是被允許的,處理器便會根據Call Gate Descriptor的Segment Selector欄位找出對應的程式區段,如果又涉及特權等級轉換,處理器會將原本使用中的堆疊切換至目標特權等級的堆疊。舉例來說,假設目前程式區段運行於Ring 3,處理器便使用Ring 3的堆疊,當使用Call Gate欲轉換至Ring 0的程式區段。如果此要求是被允許的,則處理器會跳至Ring 0的程式區段並使用Ring 0的堆疊,因此程式設計員必須先設定Ring 0堆疊 (ESP0與SS0),這就是為什麼必須設定TSS的ESP與SS0欄位的原因。
  8. 跳至GDT之Call Gate的描述子 (Descriptor)。
  9. 根據Call Gate描述子的Segment Selector欄位找出對應的CS。
  10. 將Msg3輸出至螢幕並返回。
  11. 返回Ring 3程式區段,並執行一無窮迴圈。
QEMU測試結果

【Reference】
[1] Solrex - 使用開源軟體-自己動手寫作業系統
[2] Intel 64 and IA-32 Architectures. Software Developer's Manual. Volume 3A
[3] 30天打造OS!作業系統自作入門
[4] Jserv's Blog
[5] X86 開機流程小記
[6] Linux assemblers: A comparison of GAS and NASM
[7] linux-source-2.6.31

2010年12月8日 星期三

[打造簡易作業系統 - 以GNU Assembler組合語言撰寫] (六) 簡介Call Gate

Call Gate主要目的用來將一程式的特權等級 (Privilege Level) 轉換至另一個特權等級。舉例來說,Linux使用者利用ioctl()系統呼叫從user space (Privilege 3)進入kernel space (Privilege 0)。

Note
本篇範例程式僅示範如何使用call gate,並如何利用call gate呼叫所對應的程式碼片段,所有的程式碼都運行於privilege 0。對於特權等級的轉換 (Privilege 3 -> Privilege 0),留待往後的文章再詳加探討。本篇文章僅簡單地介紹Call Gate,詳盡介紹請參考Intel 64 and IA-32 Architectures. Software Developer's Manual. Volume 3A之5.8節。

Call-Gate Descriptor之介紹
參考Figure 1,其欄位如下所述:
  1. Offset: 代表所指定的程式區段 (Code Segment)進入點 (Entry Point),通常都設為0。
  2. Segment Selector: 此區段選擇器指定所要存取的程式區段 (Code Segment)。如此說明或許有點模糊,請參考Figure 2,此圖紅色數字方塊展示出設定Call Gate的必要程序,因此設定Call Gate需要三道步驟:A. 設定Call Gate Descriptor相關欄位,其欄位設定值如Table 1所示。B. 設定對應之程式區段。C. 設定Call-Gate之區段選擇器。
  3. Param. Count: 指定呼叫程序 (Calling Procedure)欲傳遞幾個參數給被呼叫的程序 (Called Procedure)。
  4. DPL (Descriptor Privilege Level): 此區段描述子之特權等級,其DPL與RPL特權等級之檢查請參考Intel 64 and IA-32 Architectures. Software Developer's Manual. Volume 3A之Figure 5-11。
  5. P: 此Call-Gate描述子是否有有效 (有效: P=1, 無效: P=0)。

Figure 1. Call-Gate Descriptor


Figure 2. Steps for Configuring Call Gate


Table 1. Field Value Configuration for Call-Gate Descriptor

Boot Loader程式碼
請參考此篇文章的"Boot Loader 程式碼"。

作業系統程式碼
Figure 3為作業系統程式碼,此作業系統程式碼運行於32位元保護模式,程式說明如下:

  1. 定義七個Segment Descriptor (LABEL_GDT_NULL、LABEL_GDT_CODE、LABEL_GDT_DATA、LABEL_GDT_VIDEO、LABEL_GDT_LDT、LABEL_GDT_CG_CODE與LABEL_GDT_CG)。其中VIDEO的基底位址為0xB8000,詳情請參考Printing to Screen。接著定義GDT的長度、定義Code、Data、VIDEO與LDT的segment selector、定義輸出的字串、定義GDTPtr與定義LDT表。
  2. LABEL_GDT_CG為Call Gate Descriptor,其儲存在GDT。此設定步驟為Figure 2的1號紅色方塊。
  3. LABEL_GDT_CG_CODE為Call Gate Descriptor欄位Segment Selector所指定的地方,也就是Call Gate所對應的區段程式碼。此設定步驟為Figure 2的2號紅色方塊。
/* os.S
*
*/

#include "pm.h"

.code16
.text
jmp os_main

# Segment descritors for GDT
LABEL_GDT_NULL: SEG_DESC 0, 0, 0
LABEL_GDT_CODE: SEG_DESC 0, (PECode32Len - 1), (DESC_ATTR_TYPE_CD_ER | DESC_ATTR_D)
LABEL_GDT_DATA: SEG_DESC 0, (DataLen - 1), (DESC_ATTR_TYPE_CD_RW)
LABEL_GDT_VIDEO: SEG_DESC 0xB8000, 0xFFFF, (DESC_ATTR_TYPE_CD_RW)
LABEL_GDT_LDT: SEG_DESC 0, (LDTLen - 1), (DESC_ATTR_TYPE_LDT)
LABEL_GDT_CG_CODE: SEG_DESC 0, (CG_CODE32_LEN - 1), (DESC_ATTR_TYPE_CD_ER | DESC_ATTR_D)
LABEL_GDT_CG: CALL_GATE SegSelectorCGCODE, 0, 0, (GATE_CG_ATTR)

# The length of GDT
.set GdtLen, (. - LABEL_GDT_NULL)

# Segment selectors
.set SegSelectorCode32, (LABEL_GDT_CODE - LABEL_GDT_NULL)
.set SegSelectorData, (LABEL_GDT_DATA - LABEL_GDT_NULL)
.set SegSelectorVideo, (LABEL_GDT_VIDEO - LABEL_GDT_NULL)
.set SegSelectorLDT, (LABEL_GDT_LDT - LABEL_GDT_NULL)
.set SegSelectorCGCODE, (LABEL_GDT_CG_CODE - LABEL_GDT_NULL)
.set SegSelectorCG, (LABEL_GDT_CG - LABEL_GDT_NULL)

# data segment
LABEL_DATA:
Msg1: .ascii "Welcome to Protect mode in GDT.\0"
Msg2: .ascii "Welcome to Protect mode in LDT.\0"
Msg3: .ascii "Welcome to Protect mode through Call Gate.\0"

.set Msg1Offset, (Msg1 - LABEL_DATA)
.set Msg2Offset, (Msg2 - LABEL_DATA)
.set Msg3Offset, (Msg3 - LABEL_DATA)

.set DataLen, (. - LABEL_DATA)

# GDTR pointer
LABEL_GDTR:
.2byte (GdtLen - 1) # Limit field
.4byte 0 # base field

# LDT information
LABEL_LDT:
LABEL_LDT_ENTRY: SEG_DESC 0, (LDT_CODE32_LEN - 1), (DESC_ATTR_TYPE_CD_E | DESC_ATTR_D)

# length of LDT
.set LDTLen, (. - LABEL_LDT)

# LDT selector
.set SegSelectorLDTCode32, (LABEL_LDT_ENTRY - LABEL_LDT + SA_TIL)


# real-mode OS code
os_main:
mov %cs, %ax
mov %ax, %ds
mov %ax, %ss
mov %ax, %es


/* Set gdt for code segment */
InitSegDescriptor LABEL_PE_CODE32, LABEL_GDT_CODE
InitSegDescriptor LABEL_DATA, LABEL_GDT_DATA
InitSegDescriptor LABEL_LDT, LABEL_GDT_LDT
InitSegDescriptor LABEL_PE_LDT_CODE32, LABEL_LDT_ENTRY
InitSegDescriptor LABEL_PE_CG_CODE32, LABEL_GDT_CG_CODE

/* Set GDTR */
xor %ax, %ax
mov %cs, %ax
shl $4, %eax
addl $LABEL_GDT_NULL, %eax
movl %eax, (LABEL_GDTR + 2)

/* Enable A20 line */
xor %ax, %ax
in $0x92, %al
or $2, %al
out %al, $0x92

cli

/* Load the GDT base address and limit from memory into the GDTR register */
lgdt LABEL_GDTR

/* Enable protect mode */
movl %cr0, %eax
orl $1, %eax
movl %eax, %cr0

/* Jump to protected-mode OS code */
ljmp $SegSelectorCode32, $0

LABEL_PE_CG_CODE32:
.code32
mov $(SegSelectorData), %ax
mov %ax, %ds
mov $(SegSelectorVideo), %ax
mov %ax, %gs

xorl %esi, %esi
xorl %edi, %edi
movl $Msg3Offset, %esi
movl $((80 * 11 + 0) * 2), %edi
movb $0xC, %ah

cg_dump_str:
lodsb
andb %al, %al
jz cg_fin
mov %ax, %gs:(%edi)
addl $2, %edi
jmp cg_dump_str

cg_fin:
lret

.set CG_CODE32_LEN, (. - LABEL_PE_CG_CODE32)

LABEL_PE_LDT_CODE32:
.code32
# invoke a procedure call throught a call-gate.
lcall $(SegSelectorCG), $0

mov $(SegSelectorData), %ax
mov %ax, %ds
mov $(SegSelectorVideo), %ax
mov %ax, %gs

xorl %esi, %esi
xorl %edi, %edi
movl $Msg2Offset, %esi
movl $((80 * 13 + 0) * 2), %edi
movb $0xC, %ah

ldt_dump_str:
lodsb
andb %al, %al
jz ldt_fin
mov %ax, %gs:(%edi)
addl $2, %edi
jmp ldt_dump_str

ldt_fin:
jmp .

.set LDT_CODE32_LEN, (. - LABEL_PE_LDT_CODE32)

# protected-mode OS code in GDT
LABEL_PE_CODE32:
.code32
/* Load data segment selector */
mov $(SegSelectorData), %ax
mov %ax, %ds

/* Load Video segment selector */
mov $(SegSelectorVideo), %ax
mov %ax, %gs

/* Output the data */
xorl %esi, %esi
xorl %edi, %edi
movl $Msg1Offset, %esi
movl $((80 * 10 + 0) * 2), %edi
movb $0xC, %ah

dump_str:
lodsb
andb %al, %al
jz fin
mov %ax, %gs:(%edi)
addl $2, %edi
jmp dump_str

fin:
/* Load LDT selector */
mov $(SegSelectorLDT), %ax

/* Load LDT selector in GDT to LDT register */
lldt %ax

/* Jump to code segment in LDT */
ljmp $(SegSelectorLDTCode32), $0

.set PECode32Len, (. - LABEL_PE_CODE32)

.ascii "Welcome to OS context!"
.byte 0

.org 0x400, 0x41 # fill characters with 'A'. Sector 2


Figure 3. Operating System Code


pm.h標頭檔
/* pm.h
*
* Adrian Huang (adrianhuang0701@gmail.com)
*/
.macro SEG_DESC Base, Limit, Attr
.2byte (\Limit & 0xFFFF)
.2byte (\Base & 0xFFFF)
.byte ((\Base >> 16) & 0xFF)
.2byte ((\Attr & 0xF0FF) | ((\Limit >> 8) & 0x0F00))
.byte ((\Base >> 24) & 0xFF)
.endm

.macro InitSegDescriptor OFFSET GDT_SEG_ADDR
xor %ax, %ax
mov %cs, %ax
shl $4, %eax
addl $(\OFFSET), %eax
movw %ax, (\GDT_SEG_ADDR + 2)
shr $16, %eax
movb %al, (\GDT_SEG_ADDR + 4)
movb %ah, (\GDT_SEG_ADDR + 7)

.endm

.macro CALL_GATE SegSelector, Offset, ParamCount, Attr
.2byte (\Offset & 0xFFFF)
.2byte (\SegSelector)
.byte (\ParamCount)
.byte (\Attr)
.2byte ((\Offset >> 16) & 0xFFFF)
.endm

.set DESC_ATTR_TYPE_LDT, 0x82 /* LDT Segment */
.set DESC_ATTR_TYPE_CG, 0x8C /* Call-Gate Segment */
.set DESC_ATTR_TYPE_CD_ER, 0x9A /* Code segment with Execute/Read */
.set DESC_ATTR_TYPE_CD_E, 0x98 /* Code segment with Execute Only */
.set DESC_ATTR_TYPE_CD_RW, 0x92 /* Data segment with R/W */
.set DESC_ATTR_D, 0x4000 /* 32-bit segment */

/* Selector Attribute */
.set SA_TIL, 0x4
.set SA_RPL0, 0x0
.set SA_RPL1, 0x1
.set SA_RPL2, 0x2
.set SA_RPL3, 0x3

/* The attribute of call gate */
.set GATE_CG_ATTR, 0x8C



編譯程式碼
下圖為編譯的Makefile。
LD=ld
CC=gcc

all: boot_loader.bin

boot_loader.bin: boot_loader.o os.o
${LD} -Ttext=0x7C00 -s $< -o $@ --oformat binary ${LD} -Ttext=0x0 -s os.o -o os.bin --oformat binary cat os.bin >> $@

boot_loader.o:
${CC} -c boot_loader.S

os.o:
${CC} -c os.S

clean:
rm -f boot_loader.o os.o os.bin boot_loader.bin

其編譯訊息如下所示:
adrian@adrian-desktop:~/working/build_os/my_ex/blog/pe-call-gate-same-priv$ make clean all
rm -f boot_loader.o os.o os.bin boot_loader.bin
gcc -c boot_loader.S
gcc -c os.S
ld -Ttext=0x7C00 -s boot_loader.o -o boot_loader.bin --oformat binary
ld -Ttext=0x0 -s os.o -o os.bin --oformat binary
ld: warning: cannot find entry symbol _start; defaulting to 0000000000000000
cat os.bin >> boot_loader.bin
adrian@adrian-desktop:~/working/build_os/my_ex/blog/pe-call-gate-same-priv$




QEMU測試結果



【Reference】
[1] Solrex - 使用開源軟體-自己動手寫作業系統
[2] Intel 64 and IA-32 Architectures. Software Developer's Manual. Volume 3A
[3] 30天打造OS!作業系統自作入門
[4] Jserv's Blog
[5] X86 開機流程小記
[6] Linux assemblers: A comparison of GAS and NASM
[7] linux-source-2.6.31

2010年11月2日 星期二

現今最受歡迎的Linux Distribution

今天在網路上看到很有興趣的Linux Distribution搜尋排名比較,請參考原文網址,其中DistroWatch根據使用者點閱Linux Distribution網頁所得出的結果如下:

1. Ubuntu
2. Fedora
3. Mint
4. OpenSUSE
5. Debian
6. PCLinuxOS
7. Mandriva
8. Sabayon
9. Arch
10. Puppy

然而,此統計數據以Linux Desktop OS Distribution為基礎,所以Android沒在排名之中。

於是,作者利用Google Trend所得出的結果,則是Android排名第一。

2010年10月11日 星期一

[打造簡易作業系統 - 以GNU Assembler組合語言撰寫] (五) 使用Local Descriptor Table (LDT)

上篇文章使用Global Descriptor Table (GDT)儲存Code Segment (CS)與Data Segment (DS)的資訊,以便能在保護模式下成功地執行程式碼與存取資料。本篇說明如何設定LDT,以便能執行位於LDT的程式碼。

LDT之介紹
LDT實現作業系統多個程序功能 (multiple processes),其特色為各個程序有自己的位址空間彼此互不干擾,每個程序會有各自的LDT,當作業系統欲執行某一程序時,作業系統會找出其對應的LDT,以便能執行該程序。詳細資料請參考Local Descriptor Table

LDT相關設定
LDT設定分成兩大步驟如下所述:

1. 在GDT表裡設定一個LDT entry (Configure a LDT Entry in GDT)
參考Figure 1,其每個欄位所應設定值如Table 1所示:


Figure 1. Segment Descriptor


Table 1. LDT entry configuration in GDT

2. 設定LDT表

LDT表設定code segment/data segment相關資訊,其設定值如Table 2所示。


Table 2. LDT entry configuration in LDT

Boot Loader程式碼
請參考上篇文章的"Boot Loader 程式碼"。

作業系統程式碼
Figure 2為作業系統程式碼,此作業系統程式碼運行於32位元保護模式,一開始定義三個Segment Descriptor (NULL、CODE32、VIDEO與LDT),其中VIDEO的基底位址為0xB8000,詳情請參考Printing to Screen。接著定義GDT的長度、定義Code32、Data、VIDEO與LDT的segment selector、定義輸出的字串、定義GDTPtr與定義LDT表。

/* os.S
*
*/

#include "pm.h"

.code16
.text
jmp os_main

# Segment descritors for GDT
LABEL_GDT_NULL: SEG_DESC 0, 0, 0
LABEL_GDT_CODE: SEG_DESC 0, (PECode32Len - 1), (DESC_ATTR_TYPE_CD_ER | DESC_ATTR_D)
LABEL_GDT_DATA: SEG_DESC 0, (DataLen - 1), (DESC_ATTR_TYPE_CD_RW)
LABEL_GDT_VIDEO: SEG_DESC 0xB8000, 0xFFFF, (DESC_ATTR_TYPE_CD_RW)
LABEL_GDT_LDT: SEG_DESC 0, (LDTLen -1), (DESC_ATTR_TYPE_LDT)

# The length of GDT
.set GdtLen, (. - LABEL_GDT_NULL)

# Segment selectors
.set SegSelectorCode32, (LABEL_GDT_CODE - LABEL_GDT_NULL)
.set SegSelectorData, (LABEL_GDT_DATA - LABEL_GDT_NULL)
.set SegSelectorVideo, (LABEL_GDT_VIDEO - LABEL_GDT_NULL)
.set SegSelectorLDT, (LABEL_GDT_LDT - LABEL_GDT_NULL)

# data segment
LABEL_DATA:
Msg1: .ascii "Welcome to Protect mode in GDT.\0"
Msg2: .ascii "Welcome to Protect mode in LDT.\0"
Msg3: .ascii "This is signed by Adrian.\0"

.set Msg1Offset, (Msg1 - LABEL_DATA)
.set Msg2Offset, (Msg2 - LABEL_DATA)
.set Msg3Offset, (Msg3 - LABEL_DATA)

.set DataLen, (. - LABEL_DATA)

# GDTR pointer
LABEL_GDTR:
.2byte (GdtLen - 1) # Limit field
.4byte 0 # base field

# LDT information
LABEL_LDT:
LABEL_LDT_ENTRY: SEG_DESC 0, (LDT_CODE32_LEN - 1), (DESC_ATTR_TYPE_CD_E | DESC_ATTR_D)

# length of LDT
.set LDTLen, (. - LABEL_LDT)

# LDT selector
.set SegSelectorLDTCode32, (LABEL_LDT_ENTRY - LABEL_LDT + SA_TIL)


# real-mode OS code
os_main:
mov %cs, %ax
mov %ax, %ds
mov %ax, %ss
mov %ax, %es


/* Set gdt for code segment */
InitSegDescriptor LABEL_PE_CODE32, LABEL_GDT_CODE
InitSegDescriptor LABEL_DATA, LABEL_GDT_DATA
InitSegDescriptor LABEL_LDT, LABEL_GDT_LDT
InitSegDescriptor LABEL_PE_LDT_CODE32, LABEL_LDT_ENTRY

/* Set GDTR */
xor %ax, %ax
mov %cs, %ax
shl $4, %eax
addl $LABEL_GDT_NULL, %eax
movl %eax, (LABEL_GDTR + 2)

/* Enable A20 line */
xor %ax, %ax
in $0x92, %al
or $2, %al
out %al, $0x92

cli

/* Load the GDT base address and limit from memory into the GDTR register */
lgdt LABEL_GDTR

/* Enable protect mode */
movl %cr0, %eax
orl $1, %eax
movl %eax, %cr0

/* Jump to protected-mode OS code */
ljmp $SegSelectorCode32, $0


LABEL_PE_LDT_CODE32:
.code32
mov $(SegSelectorData), %ax
mov %ax, %ds
mov $(SegSelectorVideo), %ax
mov %ax, %gs

xorl %esi, %esi
xorl %edi, %edi
movl $Msg2Offset, %esi
movl $((80 * 11 + 0) * 2), %edi
movb $0xC, %ah

ldt_dump_str:
lodsb
andb %al, %al
jz ldt_fin
mov %ax, %gs:(%edi)
addl $2, %edi
jmp ldt_dump_str

ldt_fin:
jmp .

.set LDT_CODE32_LEN, (. - LABEL_PE_LDT_CODE32)

# protected-mode OS code in GDT
LABEL_PE_CODE32:
.code32
/* Load data segment selector */
mov $(SegSelectorData), %ax
mov %ax, %ds

/* Load Video segment selector */
mov $(SegSelectorVideo), %ax
mov %ax, %gs

/* Output the data */
xorl %esi, %esi
xorl %edi, %edi
movl $Msg1Offset, %esi
movl $((80 * 10 + 0) * 2), %edi
movb $0xC, %ah

dump_str:
lodsb
andb %al, %al
jz fin
mov %ax, %gs:(%edi)
addl $2, %edi
jmp dump_str

fin:
/* Load LDT selector */
mov $(SegSelectorLDT), %ax

/* Load LDT selector in GDT to LDT register */
lldt %ax

/* Jump to code segment in LDT */
ljmp $(SegSelectorLDTCode32), $0

.set PECode32Len, (. - LABEL_PE_CODE32)

os_msg:
.ascii "Welcome to OS context!"
.byte 0

.org 0x200, 0x41 # fill characters with 'A'. Sector 2


Figure 2. Code for Operating System

16位元real mode程式碼 (os_main)中,執行若干任務如下所述:
  1. 設定LABEL_GDT_CODE的基底位址為PE_CODE32的起始位址
  2. 設定LABEL_GDT_DATA的基底位址為LABEL_DATA的起始位址
  3. 設定LABEL_GDT_LDT的基底位址為LABEL_LDT的起始位址
  4. 設定LABEL_LDT_ENTRY的基底位址為LABEL_PE_LDT_CODE32的起始位址
  5. 設定GDTPtr的基底位址為GDT的起始位址(也就是GDT_DESC_NULL)
  6. 開啟A20線路 (A20 Line)
  7. 將GDT的起始位址載入至GDTR暫存器
  8. 設定cr0暫存器的bit 0以便進入保護模式
  9. 使用ljmp指令跳至PE_CODE32程式碼
  10. 使用lldt指令將LDT的segment selector載入至LDTR (Local Descriptor Table Register),並跳至SegSelectorLDTCode32 segment selector執行LDT的程式碼。
32位元保護模式程式碼 (LABEL_PE_CODE32)利用Video segment selector將"Welcome to Protect mode in GDT."顯示在螢幕上,而LABEL_PE_LDT_CODE32則將"Welcome to Protect mode in LDT."顯示在螢幕上,用以驗證程式運作之正確性。

pm.h標頭檔
/* pm.h
*
* Adrian Huang (adrianhuang0701@gmail.com)
*/
.macro SEG_DESC Base, Limit, Attr
.2byte (\Limit & 0xFFFF)
.2byte (\Base & 0xFFFF)
.byte ((\Base >> 16) & 0xFF)
.2byte ((\Attr & 0xF0FF) | ((\Limit >> 8) & 0x0F00))
.byte ((\Base >> 24) & 0xFF)
.endm

.macro InitSegDescriptor OFFSET GDT_SEG_ADDR
xor %ax, %ax
mov %cs, %ax
shl $4, %eax
addl $(\OFFSET), %eax
movw %ax, (\GDT_SEG_ADDR + 2)
shr $16, %eax
movb %al, (\GDT_SEG_ADDR + 4)
movb %ah, (\GDT_SEG_ADDR + 7)

.endm

.set DESC_ATTR_TYPE_LDT, 0x82 /* LDT Segment */
.set DESC_ATTR_TYPE_CD_ER, 0x9A /* Code segment with Execute/Read */
.set DESC_ATTR_TYPE_CD_E, 0x98 /* Code segment with Execute Only */
.set DESC_ATTR_TYPE_CD_RW, 0x92 /* Data segment with R/W */
.set DESC_ATTR_D, 0x4000 /* 32-bit segment */

/* Selector Attribute */
.set SA_TIL, 0x4
.set SA_RPL0, 0x0
.set SA_RPL1, 0x1
.set SA_RPL2, 0x2
.set SA_RPL3, 0x3



編譯程式碼
請參照此篇的Makefile程式碼

其編譯訊息如下所示:

adrian@adrian-desktop:~/working/build_os/my_ex/04day/blog/pe-multi-seg-ldt$ make clean all
rm -f boot_loader.o os.o boot_loader.bin
gcc -c boot_loader.S
gcc -c os.S
ld -Ttext=0x7C00 -s boot_loader.o -o boot_loader.bin --oformat binary
ld -Ttext=0x0 -s os.o -o os.bin --oformat binary
ld: warning: cannot find entry symbol _start; defaulting to 0000000000000000
cat os.bin >> boot_loader.bin
adrian@adrian-desktop:~/working/build_os/my_ex/04day/blog/pe-multi-seg-ldt$


QEMU測試結果


【Reference】
[1] Solrex - 使用開源軟體-自己動手寫作業系統
[2] Intel 64 and IA-32 Architectures. Software Developer's Manual. Volume 3A
[3] 30天打造OS!作業系統自作入門
[4] Jserv's Blog
[5] X86 開機流程小記
[6] Linux assemblers: A comparison of GAS and NASM
[7] linux-source-2.6.31

2010年9月29日 星期三

[打造簡易作業系統 - 以GNU Assembler組合語言撰寫] (四) 由16位元真實模式 (Real Mode) 進入32位元保護模式 (Protect Mode)

前三篇文章所展示的程式碼都是CPU執行於真實模式 (Real Mode)。然而,一般作業系統運行於保護模式 (Protect Mode),其記憶體定址最高可至4GB (32位元)。故本文先介紹real mode與protect mode記憶體定址的方式。

Real mode與Protect mode記憶體定址介紹
Figure 1展示real mode記憶體定址方式,其觀念在於將邏輯位址(Logical Address)的區段(Segment)位址向左位移4個位元,再將其所得的位址加上位移值,如此便能轉換成線性位址 (Linear Address)。至於,邏輯位址該如何表示呢? 其表示法為"Address of Segment:Offset"
,例如: CS:IP、SS:SP、DS:SI和ES:DI,詳情請參考x86 Assembly Language。Figure 1描述一個簡單邏輯位址轉換線性位址的例子,因此不再贅述。

Figure 1. Memory Segmentation in Real Mode

Figure 2展示保護模式記憶體定址方式,其觀念在於將區段 (Segment)看成區段選擇器 (Segment Selector),用此選擇器索引出對應的Segment Descriptor,如此便能索引32位元的基底位址 (32-bit base address),然後再加上位移植,便能轉換成線性位址。

Figure 2. Memory Segment in Protect Mode

保護模式相關課題之介紹
此段落將著重介紹保護模式相關課題之介紹,包含介紹GDT/LDT (GDTR/LDTR)、Segment Descriptor、Segment Selector、 和Memory Management Register

Global and Local Descriptor Table (GDT and LDT)
當CPU運行於保護模式時,所有的記憶體存取都必須經由GDT或LDT,此表格 (GDT or LDT)存放最小單元便是Segment Descriptor。每一個Segment Descriptor都有對應的segment selector,用以索引出對應的Segment Descriptor。

GDTR and LDTR (GDT Register and LDT Register)
GDTR與LDTR用以儲存GDT與LDT的起始位置,此設定必須在進入保護模式完成設定。Figure 3展示GDTR與LDTR格式,其中GDTR包含32位元的基底位址與16位元的長度限制。而LDTR多增加了16位元的segment selector。


Figure 3. Memory Management Register

Segment Descriptor
Segment Descriptor為Descriptor Table組成的基本元素,其長度為8位元組。如Figure 4所示,可分為三大類: 1. 32位元的基底位址 (Base Address), 2. 20位元的區段限制 (Segment Limit), 3. 區段屬性。細節請參考Intel 64 and IA-32 Architectures. Software Developer's Manual. Volume 3A


Figure 4. Segment Descriptor [2]

Segment Selector
Figure 5展示Segment Selector示意圖,其目的用來索引對應的Segment Descriptor。因Descriptor Index有13個位元,故Descriptor個數最大可至8192。


Figure 5. Segment Selector

Boot Loader程式碼
/* boot_loader.S
*
* Copyright (C) 2010 Adrian Huang (adrianhuang0701@gmail.com)
*
* This code is intended to simulate a simplified boot loader. This boot
* loader loads 3 sectors into the physical memory and jumps the entry
* point of OS.
*
*/

.code16
.text

.set BOOT_SEG, 0x07C0 /* starting code segment (CS) of boot loader */
.set OS_SEG, 0x0900 /* code segment address of OS entry point */
.set OS_OFFSET, 0x0000 /* the offset address of OS entry point */

.global _start
_start:
# FAT12 file system format
jmp start_prog # jmp instruction

.byte 0x90
.ascii "ADRIAN " # OEM name (8 bytes)
.word 512 # Bytes per sector
.byte 1 # Sector per cluster
.word 1 # Reserved sector count: should be 1 for FAT12
.byte 2 # Number of file allocation tables.
.word 224 # Maximum number of root directory entries.
.word 2880 # Total sectors
.byte 0xf0 # Media descriptor:
.word 9 # Sectors per File Allocation Table
.word 18 # Sectors per track
.word 2 # Number of heads
.long 0 # Count of hidden sectors
.long 0 # Total sectors
.byte 0 # Physical driver number
.byte 0 # Reserved
.byte 0x29 # Extended boot signature
.long 0x12345678 # Serial Number
.ascii "HELLO-OS " # Volume Label
.ascii "FAT12 " # FAT file system type
.fill 18, 1, 0 # fill 18 characters with zero

start_prog:
# initialize the register with cs register
movw %cs, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
xorw %sp, %sp

cld # clear direction flag
sti # set interrupt flag

# The following code is loaded three sectors (2-4th sectors from boot.bin)
# into the physical memory 0x8000-0x85FF.
movw $OS_SEG, %ax
mov %ax, %es # ES:BX-> destination buffer address pointer
movw $OS_OFFSET, %bx
movb $2, %cl # sector


cont:
movb $0x02, %ah # Read sectors from drive
movb $0x1, %al # Sectors to read count
movb $0x0, %ch # track
movb $0x0, %dh # head
movb $0, %dl # drive

int $0x13 # trigger a interrupt 0x13 service
jc fail # the clear flag is set if the operation is failed

mov %es, %ax
addw $0x20, %ax # move to the next sector
movw %ax, %es # move to the next sector
incb %cl

cmpb $3, %cl # has finished reading 3 sectors?
jbe cont # continue to read the sector

jmp os_entry # jump to OS entry point

fail:
movw $err_msg, %si
fail_loop:
lodsb
andb %al, %al
jz end
movb $0x0e, %ah
int $0x10
jmp fail_loop


os_entry:
ljmp $OS_SEG, $OS_OFFSET # jump to os context

end:
hlt
jmp end

err_msg:
.ascii "Reading sectors operation is failed!"
.byte 0

.org 0x1FE, 0x41 # fill the rest of characters with zero until the 254th character

# Boot sector signature
.byte 0x55
.byte 0xaa


作業系統程式碼
此作業系統程式碼運行於32位元保護模式,一開始定義三個Segment Descriptor (NULL, CODE32與VIDEO),其中VIDEO的基底位址為0xB8000,詳情請參考Printing to Screen。接著定義GDT的長度、定義Code32與VIDEO的segment selector、定義GDTPtr。

16位元real mode程式碼 (os_main)中,執行若干任務如下所述:
  1. 設定Code32的基底位址為PE_CODE32的起始位址
  2. 設定GDTPtr的基底位址為GDT的起始位址(也就是GDT_DESC_NULL)
  3. 開啟A20線路 (A20 Line)
  4. 將GDT的起始位址載入至GDTR暫存器
  5. 設定cr0暫存器的bit 0以便進入保護模式
  6. 使用ljmp指令跳至PE_CODE32程式碼
32位元保護模式程式碼 (PE_CODE32)利用Video segment selector將'H'字元顯示在螢幕,用以驗證程式運作之正確性。

/* os.S
*
* Adrian Huang (adrianhuang0701@gmail.com)
*
* This code is OS context for protected-mode.
*
*/
#include "pm.h"

.code16
.text
jmp os_main

# Segment descritors for GDT
GDT_DESC_NULL: SEG_DESC 0, 0, 0
GDT_DESC_CODE32: SEG_DESC 0, (PECode32Len - 1), (DESC_ATTR_TYPE_CD_ER | DESC_ATTR_D)
GDT_DESC_VIDEO: SEG_DESC 0xB8000, 0xFFFF, (DESC_ATTR_TYPE_CD_RW)

# The length of GDT
.set GdtLen, (. - GDT_DESC_NULL)

# Segment selectors for code segment and video output
.set SegSelectorCode32, (GDT_DESC_CODE32 - GDT_DESC_NULL)
.set SegSelectorVideo, (GDT_DESC_VIDEO - GDT_DESC_NULL)

# GDTR pointer
GDTPtr:
.2byte (GdtLen - 1) # Limit field
.4byte 0 # base field

# real-mode OS code
os_main:
mov %cs, %ax
mov %ax, %ds
mov %ax, %ss
mov %ax, %es


/* Set gdt for code segment */
InitSegDescriptor PE_CODE32, GDT_DESC_CODE32

/* Set GDTR */
xor %ax, %ax
mov %cs, %ax
shl $4, %eax
addl $GDT_DESC_NULL, %eax
movl %eax, (GDTPtr + 2)

/* Enable A20 line */
xor %ax, %ax
in $0x92, %al
or $2, %al
out %al, $0x92

cli

/* Load the GDT base address and limit from memory into the GDTR register */
lgdt GDTPtr

/* Enable protect mode */
movl %cr0, %eax
orl $1, %eax
movl %eax, %cr0

/* Jump to protected-mode OS code */
ljmp $SegSelectorCode32, $0


# protected-mode OS code
PE_CODE32:
.code32
/* Load Video segment selector */
mov $(SegSelectorVideo), %ax
mov %ax, %gs

/* Output the data */
movl $((80 * 10 + 0) * 2), %edi
movb $0xC, %ah
movb $'H', %al
mov %ax, %gs:(%edi)

jmp .

.set PECode32Len, (. - PE_CODE32)

os_msg:
.ascii "Welcome to OS context!"
.byte 0

.org 0x200, 0x41 # fill characters with 'A'. Sector 2


pm.h標頭檔

/* pm.h
*
* Adrian Huang (adrianhuang0701@gmail.com)
*/


.macro SEG_DESC Base, Limit, Attr
.2byte (\Limit & 0xFFFF)
.2byte (\Base & 0xFFFF)
.byte ((\Base >> 16) & 0xFF)
.2byte ((\Attr & 0xF0FF) | ((\Limit >> 8) & 0x0F00))
.byte ((\Base >> 24) & 0xFF)
.endm

.macro InitSegDescriptor OFFSET GDT_SEG_ADDR
xor %ax, %ax
mov %cs, %ax
shl $4, %eax
addl $(\OFFSET), %eax
movw %ax, (\GDT_SEG_ADDR + 2)
shr $16, %eax
movb %al, (\GDT_SEG_ADDR + 4)
movb %ah, (\GDT_SEG_ADDR + 7)

.endm

.set DESC_ATTR_TYPE_LDT, 0x82 /* LDT Segment */
.set DESC_ATTR_TYPE_CD_ER, 0x9A /* Code segment with Execute/Read */
.set DESC_ATTR_TYPE_CD_E, 0x98 /* Code segment with Execute Only */
.set DESC_ATTR_TYPE_CD_RW, 0x92 /* Data segment with R/W */
.set DESC_ATTR_D, 0x4000 /* 32-bit segment */

/* Selector Attribute */
.set SA_TIL, 0x4
.set SA_RPL0, 0x0
.set SA_RPL1, 0x1
.set SA_RPL2, 0x2
.set SA_RPL3, 0x3

編譯程式碼
下圖為編譯的Makefile。
LD=ld
CC=gcc

all: boot_loader.bin

boot_loader.bin: boot_loader.o os.o
${LD} -Ttext=0x7C00 -s $< -o $@ --oformat binary
${LD} -Ttext=0x0 -s os.o -o os.bin --oformat binary
cat os.bin >> $@

boot_loader.o:
${CC} -c boot_loader.S

os.o:
${CC} -c os.S

clean:
rm -f boot_loader.o os.o os.bin boot_loader.bin


其編譯訊息如下所示:
adrian@adrian-desktop:~/working/build_os/my_ex/04day/pe-orig-makefile$ make all
gcc -c boot_loader.S
gcc -c os.S
ld -Ttext=0x7C00 -s boot_loader.o -o boot_loader.bin --oformat binary
ld -Ttext=0x0 -s os.o -o os.bin --oformat binary
ld: warning: cannot find entry symbol _start; defaulting to 0000000000000000
cat os.bin >> boot_loader.bin



QEMU測試結果


【Reference】
[1] Solrex - 使用開源軟體-自己動手寫作業系統
[2] Intel 64 and IA-32 Architectures. Software Developer's Manual. Volume 3A
[3] 30天打造OS!作業系統自作入門
[4] Jserv's Blog
[5] X86 開機流程小記
[6] Linux assemblers: A comparison of GAS and NASM
[7] linux-source-2.6.31

2010年8月31日 星期二

[打造簡易作業系統 - 以GNU Assembler組合語言撰寫] (三) Boot Loader + 作業系統載入實例 (CF Card)

上篇說明如何撰寫小型Boot Loader將作業系統載入至記憶體並執行該作業系統程式碼,並利用qemu實現。為了更真實性,本篇將boot loader及作業系統安裝在CF card並利用CF card開機,用以證明該boot loader及作業系統可以正確地在實體機器上運行。

將DL暫存器更改為0x80
由於上篇是使用軟碟機開機,因此在使用BIOS中斷服務0x13時 (AH=02 Read Sectors from Driver),需將DL暫存器設定為0(0代表軟碟機0,1代表軟碟機1),但因為現在要從硬碟讀取,所以需將DL設定為0x80,即底下範例程式紅色部份。

Boot Loader範例程式 (以FAT32為範例)

/* boot_loader.S
*
* Copyright (C) 2010 Adrian Huang (adrianhuang0701@gmail.com)
*
* This code is intended to simulate a simplified boot loader. This boot
* loader loads 3 sectors into the physical memory and jumps the entry
* point of OS.
*
*/

BOOT_SEG = 0x07C0 /* starting code segment (CS) of boot loader */
OS_SEG = 0x0800 /* code segment address of OS entry point */
OS_OFFSET = 0x0000 /* the offset address of OS entry point */

.code16

.section .text

.global _start
_start:
# FAT12 file system format
ljmp $BOOT_SEG, $start_prog # jmp instruction

.byte 0x90
.ascii "ADRIAN " # OEM name (8 bytes)
.word 512 # Bytes per sector
.byte 1 # Sector per cluster
.word 32 # Reserved sector count: should be 32 for FAT32
.byte 2 # Number of file allocation tables.
.word 0 # Maximum number of root directory entries. 0 for FAT32
.word 0 # Total sectors
.byte 0xf8 # Media descriptor: fix disk
.word 9 # Sectors per File Allocation Table
.word 18 # Sectors per track
.word 2 # Number of heads
.long 0 # Count of hidden sectors
.long 2030112 # Total sectors
.byte 0 # Physical driver number
.byte 0 # Reserved
.byte 0x29 # Extended boot signature
.long 0x12345678 # Serial Number
.ascii "HELLO-OS " # Volume Label
.ascii "FAT12 " # FAT file system type
.fill 18, 1, 0 # fill 18 characters with zero

start_prog:
# initialize the register with cs register
movw %cs, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
xorw %sp, %sp

cld # clear direction flag
sti # set interrupt flag

# The following code is loaded three sectors (2-4th sectors from boot.bin)
# into the physical memory 0x8000-0x85FF.
movw $OS_SEG, %ax
mov %ax, %es # ES:BX-> destination buffer address pointer
movb $2, %cl # sector


cont:
movw $0, %bx
movb $0x02, %ah # Read sectors from drive
movb $0x1, %al # Sectors to read count
movb $0x0, %ch # track
movb $0x0, %dh # head
movb $0x80, %dl # drive

int $0x13 # trigger a interrupt 0x13 service
jc fail # the clear flag is set if the operation is failed

mov %es, %ax
addw $0x20, %ax # move to the next sector
movw %ax, %es # move to the next sector
incb %cl
cmpb $3, %cl # has finished reading 3 sectors?
jbe cont # continue to read the sector

jmp os_entry # jump to OS entry point

fail:
movw $err_msg, %si
fail_loop:
lodsb
andb %al, %al
jz end
movb $0x0e, %ah
int $0x10
jmp fail_loop


os_entry:
ljmp $OS_SEG, $OS_OFFSET # jump to os context

end:
hlt

err_msg:
.ascii "Reading sectors operation is failed!"
.byte 0

.org 0x1FE, 0x41 # fill the rest of characters with zero until the 254th character

# Boot sector signature
.byte 0x55
.byte 0xaa


作業系統程式碼與編譯
請參照這篇的"作業系統程式碼"與"編譯程式碼"。

安裝boot_loader.bin安裝至CF card
adrian@adrian-mem1:~/img$ sudo dd if=./boot_loader.bin of=/dev/sda

測試結果
將該系統重開並選擇CF Card開機,其畫面如下:
DSC00165

【Reference】
1.30天打造OS!作業系統自作入門
2. Jserv's Blog
3. X86 開機流程小記
4. Linux assemblers: A comparison of GAS and NASM
5.
linux-source-2.6.31

2010年8月20日 星期五

Google將推出Chrome作業系統的平板電腦

Google將與HTC合作,以Google Chrome作業系統開發平板電腦,預計今年11月26發表。

詳見: First Chrome OS Tablet Set for Black Friday Debut

文中還提及為什麼不使用Android的原因。

2010年8月13日 星期五

Google大神開始對Linux Kernel Source貢獻了

Linux 2.6.35有Google大神的程式碼了!! (跪拜0rz......)
其主要提升網路封包處理速度
詳見: Linux 2.6.35 Includes Speedy Google Code, Less Bloat

2010年8月11日 星期三

[打造簡易作業系統 - 以GNU Assembler組合語言撰寫] (二) Boot Loader + 作業系統載入實例 (QEMU)

上篇說明如何撰寫開機Hello World,本篇文章說明如何撰寫簡單的Boot Loader跟一個只會印出訊息的作業系統。

小型Boot Loader設計概念
筆者所撰寫的小型Boot Loader於BIOS開機成功後,會被載入至實體記憶體位址0x7C00並跳至此位址執行boot loader的程式碼,此boot loader程式碼會將作業系統程式碼 (僅三個磁區),載入實體記憶體位址0x8000並跳至此位址執行作業系統的程式碼,然而此作業系統別無功能,僅會印出簡單的訊息。如此便能模擬一般boot loader載入作業系統的程序。

圖一為筆者所編譯出來的plain binary file,此程式碼僅有四個磁區 (共2048 bytes),0x0-0x1ff為boot loader磁區,0x200-0x7ff為作業系統的三個磁區,雖然,真正的作業系統程式碼在0x200-0x3ff,其它兩個磁區僅填入字元'B'與'C',但筆者還是把這三個磁區稱為作業系統程式碼,因為boot loader會將這三個磁區載入實體記憶體位址0x8000。

圖一、Boot + OS Binary Image Layout

圖二為boot loader將作業系統載入實體記憶體位址示意圖,至於為什麼會選擇0x8000開始存放作業系統程式碼,其原因是x86系統規範位址0x7E00-0x7FFFF為conventional memory,因此筆者就挑0x8000來存放程式碼。

圖二、Boot Loader + OS Physical Memory Layout

Boot Loader程式碼
下圖為Boot Loader程式碼,其運作原理在此稍作描述。首先,boot_loader透過.byte、.word、.long跟.ascii等指令將此磁區描述為一個FAT12檔案系統。接著,利用中斷服務編號0x13將作業系統的三個磁區讀入0x8000實體記憶體位址。如果讀取失敗的話,則利用中斷服務編號0x010印出錯誤訊息。特別要提出的是,程式碼使用兩次遠程跳躍 (Far Dump),其原型ljmp code_segment_address, relative_address,例如: ljmp $BOOT_SEG, $start_prog代表code segment設定為0x07C0加上start_prog標籤的位址,即0x7C00+start_prog位址,此為Intel x86 CPU memory segmentation機制。透過此設定,boot loader程式碼便能正確地在0x7C00位址執行。另一個ljmp,ljmp $OS_SEG, $OS_OFFSET,因為boot loader將作業系統程式碼放在0x8000實體記憶體位址,因此code segment必須設為0x0800,以便讓作業系統程式碼可以正確地執行。
/* boot_loader.S
*
* Copyright (C) 2010 Adrian Huang (adrianhuang0701@gmail.com)
*
* This code is intended to simulate a simplified boot loader. This boot
* loader loads 3 sectors into the physical memory and jumps the entry
* point of OS.
*
*/

BOOT_SEG = 0x07C0 /* starting code segment (CS) of boot loader */
OS_SEG = 0x0800 /* code segment address of OS entry point */
OS_OFFSET = 0x0000 /* the offset address of OS entry point */

.code16

.section .text

.global _start
_start:
# FAT12 file system format
ljmp $BOOT_SEG, $start_prog # jmp instruction

.byte 0x90
.ascii "ADRIAN " # OEM name (8 bytes)
.word 512 # Bytes per sector
.byte 1 # Sector per cluster
.word 1 # Reserved sector count: should be 1 for FAT12
.byte 2 # Number of file allocation tables.
.word 224 # Maximum number of root directory entries.
.word 2880 # Total sectors
.byte 0xf0 # Media descriptor
.word 9 # Sectors per File Allocation Table
.word 18 # Sectors per track
.word 2 # Number of heads
.long 0 # Count of hidden sectors
.long 2880 # Total sectors: 18 (sectors per track) * 2 (heads) * 80 (sectors) = 2880
.byte 0 # Physical driver number
.byte 0 # Reserved
.byte 0x29 # Extended boot signature
.long 0x12345678 # Serial Number
.ascii "HELLO-OS " # Volume Label
.ascii "FAT12 " # FAT file system type
.fill 18, 1, 0 # fill 18 characters with zero

start_prog:
# initialize the register with cs register
movw %cs, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
xorw %sp, %sp

cld # clear direction flag
sti # set interrupt flag

# The following code is loaded three sectors (2-4th sectors from boot.bin)
# into the physical memory 0x8000-0x85FF.
movw $OS_SEG, %ax
mov %ax, %es # ES:BX-> destination buffer address pointer
movb $2, %cl # sector


cont:
movw $0, %bx
movb $0x02, %ah # Read sectors from drive
movb $0x1, %al # Sectors to read count
movb $0x0, %ch # track
movb $0x0, %dh # head
movb $0, %dl # drive

int $0x13 # trigger a interrupt 0x13 service
jc fail # the clear flag is set if the operation is failed

mov %es, %ax
addw $0x20, %ax # move to the next sector
movw %ax, %es # move to the next sector
incb %cl
cmpb $3, %cl # has finished reading 3 sectors?
jbe cont # continue to read the sector

jmp os_entry # jump to OS entry point

fail:
movw $err_msg, %si
fail_loop:
lodsb
andb %al, %al
jz end
movb $0x0e, %ah
int $0x10
jmp fail_loop


os_entry:
ljmp $OS_SEG, $OS_OFFSET # jump to os context

end:
hlt

err_msg:
.ascii "Reading sectors operation is failed!"
.byte 0

.org 0x1FE, 0x41 # fill the rest of characters with zero until the 254th character

# Boot sector signature
.byte 0x55
.byte 0xaa


作業系統程式碼
此段程式碼僅將訊息輸出至螢幕上,所以不再贅述。
/* os.S
*
* Copyright (C) 2010 Adrian Huang (adrianhuang0701@gmail.com)
*
* This code is OS context.
*
*/
.code16
.section .text
.global main

main:
movw %cs, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
xorw %sp, %sp
cld # clear direction flag
sti # set interrupt flag

movw $os_msg, %si
load_msg:
lodsb
andb %al, %al
jz os_fin
movb $0x0e, %ah
int $0x10
jmp load_msg

os_fin:
hlt
jmp os_fin

os_msg:
.ascii "Welcome to OS context!"
.byte 0

.org 0x200, 0x41 # fill characters with 'A'. Sector 1
.org 0x400, 0x42 # fill characters with 'B'. Sector 2
.org 0x600, 0x43 # fill characters with 'C'. Sector 3

編譯程式碼
下圖為編譯的Makefile。
LD=ld
CC=gcc

all: boot_loader.bin

boot_loader.bin: boot_loader.o os.o
${LD} -Ttext=0x0 -s $< -o $@ --oformat binary
${LD} -Ttext=0x0 -s os.o -o os.bin --oformat binary
cat os.bin >> $@

boot_loader.o:
${CC} -c boot_loader.S

os.o:
${CC} -c os.S

clean:
rm -f boot_loader.o boot_loader.bin os.o

其編譯訊息如下所示:
adrian@adrian-desktop:~/working/build_os/my_ex/boot_loader$ make clean all
rm -f boot_loader.o boot_loader.bin os.o
gcc -c boot_loader.S
gcc -c os.S
ld -Ttext=0x0 -s boot_loader.o -o boot_loader.bin --oformat binary
ld -Ttext=0x0 -s os.o -o os.bin --oformat binary
ld: warning: cannot find entry symbol _start; defaulting to 0000000000000000
cat os.bin >> boot_loader.bin
adrian@adrian-desktop:~/working/build_os/my_ex/boot_loader$


測試結果


為了驗證作業系統的程式碼正確地載入實體記憶體位址0x8000,筆者利用xxd工具將boot_loader.bin dump出來,下圖為其結果。紅色框框為作業系統程式碼的十六進制碼。

下圖中,筆者利用qemu提供的xp工具將0x8000-0x8010的內容dump出來,用以跟上圖0x200-0x210比對,比較上、下這兩張圖,可以證明作業系統程式碼正確地被載入至0x8000。



2010年8月3日 星期二

[打造簡易作業系統 - 以GNU Assembler組合語言撰寫] (一) 開機Hello World實例

小弟最近想嘗試利用GAS(GNU Assembler)組合語言撰寫非常小型的作業系統,本篇文章說明如何利用GAS組合語言在終端機上印出Hello World.

簡介x86 CPU開機流程
x86 CPU開機後,首要之事會先跳至0xFFFF0執行BIOS ROM的程式,當BIOS測試程序通過後,BIOS便會把執行權交給下一個程式 (boot loader或一支小程式),BIOS會將該程式
(通常為一個磁區[Sector]大小,即512 bytes)載入記憶體0x7C00位置,並跳至0x7C00執行該段程式碼。該磁區被稱為MBR,BIOS會檢查該磁區最後兩個位完組必須為0x55AA,否則該磁區該被視為無效的MBR。

所以,首要之事就是利用GAS撰寫一支大小為512位元組的二進位檔 (Binary File),此檔需具備底下功能:
  • 檔案系統,如: FAT12, FAT16, FAT32, NTFS等等。
  • 利用BIOS中斷號碼0x10將資料寫至螢幕。
  • 在最後兩個位元組寫入0x55AA以便通過BIOS識別。
底下為原始碼:
    .code16


.section .text
.global main
main:
# FAT12 file system format
jmp start_prog # jmp instruction
.byte 0x90
.ascii "ADRIAN " # OEM name (8 bytes)
.word 512 # Bytes per sector
.byte 1 # Sector per cluster
.word 1 # Reserved sector count: should be 1 for FAT12
.byte 2 # Number of file allocation tables.
.word 224 # Maximum number of root directory entries.
.word 2880 # Total sectors
.byte 0xf0 # Media descriptor:
.word 9 # Sectors per File Allocation Table
.word 18 # Sectors per track
.word 2 # Number of heads
.long 0 # Count of hidden sectors
.long 2880 # Total sectors: 18 (sectors per track) * 2 (heads) * 80 (sectors) = 2880
.byte 0 # Physical driver number
.byte 0 # Reserved
.byte 0x29 # Extended boot signature
.long 0x12345678 # Serial Number
.ascii "HELLO-OS " # Volume Label
.ascii "FAT12 " # FAT file system type
.fill 18, 1, 0 # fill 18 characters with zero

start_prog:
movw $0, %ax # Initialize register
movw %ax, %ss
movw %ax, %ds
movw %ax, %es

movw $msg, %si # move the address of msg to SI


loop:
movb $0xe, %ah
movb (%si), %al # move the first character of msg to AL register
cmpb $0, %al
je fin
int $0x10 # write the specific character to console
addw $1, %si
jmp loop

fin:
# do nothing

msg:
.ascii "Hello, World! This is Adrian Huang."
.byte 0

.org 0x1FE, 0x00 # fill the rest of characters with zero until the 254th character

# Boot sector signature
.byte 0x55
.byte 0xaa

Compile and Link
以"gcc -c"將.S組合語言轉為成object file
adrian@adrian-desktop:~/working/build_os/my_ex/02day/helloos4$ ls

hello.S
adrian@adrian-desktop:~/working/build_os/my_ex/02day/helloos4$ gcc -c hello.S
adrian@adrian-desktop:~/working/build_os/my_ex/02day/helloos4$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
adrian@adrian-desktop:~/working/build_os/my_ex/02day/helloos4$

再經由ld連結器將此object轉換成plain binary file
$ ld -Ttext=0x7C00 hello.o -o hello.bin --oformat binary

$ file hello.bin
hello.bin: DOS floppy 1440k, x86 hard disk boot sector


利用xxd工具觀察helo.bin格式 (以十六進制)
adrian@adrian-desktop:~/working/build_os/my_ex/02day/helloos4$ xxd hello.bin

0000000: eb4e 9041 4452 4941 4e20 2000 0201 0100 .N.ADRIAN .....
0000010: 02e0 0040 0bf0 0900 1200 0200 0000 0000 ...@............
0000020: 400b 0000 0000 2978 5634 1248 454c 4c4f @.....)xV4.HELLO
0000030: 2d4f 5320 2020 4641 5431 3220 2020 0000 -OS FAT12 ..
0000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000050: b800 008e d08e d88e c0be 6b7c b40e 8a04 ..........k|....
0000060: 3c00 7407 cd10 83c6 01eb f148 656c 6c6f <.t........Hello
0000070: 2c20 576f 726c 6421 2054 6869 7320 6973 , World! This is
0000080: 2041 6472 6961 6e20 4875 616e 672e 0000 Adrian Huang...
0000090: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000a0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000b0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000c0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000d0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000e0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000f0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000100: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000110: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000120: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000130: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000140: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000150: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000160: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000170: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000180: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000190: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00001a0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00001b0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00001c0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00001d0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00001e0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00001f0: 0000 0000 0000 0000 0000 0000 0000 55aa ..............U.



接著,使用qemu驗證hello.bin

qemu

為了讓此範例更真實,筆者有一台機器備有CF Card,將hello.bin透過dd工具寫進此CF Card最前面的512 bytes, 命令如下:
adrian@adrian-mem1:~/img$ sudo dd if=./hello.bin of=/dev/sda

[sudo] password for adrian:
1+0 records in
1+0 records out
512 bytes (512 B) copied, 0.00124243 s, 412 kB/s
adrian@adrian-mem1:~/img$


將該系統重開並選擇CF Card開機,其畫面如下:

DSC00153

【Reference】
1.30天打造OS!作業系統自作入門
2. Jserv's Blog
3. X86 開機流程小記
4. Linux assemblers: A comparison of GAS and NASM

2010年3月19日 星期五

簡介Linux Block I/O Layer (三) - I/O Path

此系列文章最後講解Block I/O Layer I/O Path運作原理。圖一為I/O Path簡易圖,檔案系統核心經由submit_bio函式將該bio交給Block I/O Layer的generic_make_request和__generic_make_request函式,緊接著呼叫__make_request_fn callback函式 (對應至__make_request()),此函式主要目的用來檢查此bio是否可以跟尚未處理的request結構的bio成員結合在一起 (merge),如果不行的話,則另外配置一個新的request結構並將其bio安插至該reqeust結構,再將該request交給I/O Scheduler,最後Block I/O Layer經由request_fn() callback function將待處理的request交給SCSI子系統。


                               圖一、Block I/O Layer之I/O Path簡易圖


下圖二為Linux Block I/O Layer核心I/O Path示意圖,此圖以ReiserFS為例,此圖對每個函式有詳細的解釋,所以在此不再解釋,請參照圖二。


                               圖二、Block I/O Layer之I/O Path


【Reference】
1. Linux Device Driver, third edition
2. Linux Kernel Source 2.6.31
3. Request-based Device-mapper multipath and Dynamic load balancing
4. Understanding the Linux Kernel, Third Edition - Chapter 14. Block Device Drivers

2010年3月18日 星期四

簡介Linux Block I/O Layer (二) - 探討BIO (Block I/O) and Request 結構

上一篇文章簡單介紹page, bio和request結構的定位,本篇文章著重於探討bio與request結構是如何串起來的。底下將分別介紹reqeust queue、request、bio與bio_vec等資料結構。首先,下圖展示這四個資料結構的關係。



Request Queue
Request Queue用來將待處理的request串成一個雙向的鏈結串列,此結構 (struct request_queue)定義在include/linux/blkdev.h檔頭檔。

Request
一個request資料結構,即表示一次block I/O傳輸請求。結構裡的queuelist便是將整個request串起來的成員,bio成員代表該request所欲傳輸block I/O個數,buffer成員代表當前資料傳輸的buffer區塊。request資料結構裡還有許多其它成員,詳見include/linux/blkdev.h標頭檔。

BIO (Block I/O)
當block I/O layer上層 (可參考此篇文章的圖,Ex: 檔案系統或虛擬記憶體子系統)欲傳輸某一區塊時,該層便會產生一個bio結構並交由Block I/O Layer,Block I/O Layer將新請求的bio併入現有的request結構裡 (前提是該request結構裡的bio所欲傳輸的區塊磁區剛好跟新請求的bio所欲傳輸的區塊磁區相近,如此變能發揮更大的傳輸效益),或者產生一個新的request結構並將此bio併入此request結構,此結構 (struct request_queue)定義在include/linux/bio.h標頭檔。

bio_vec (BIO Vector)
bio結構裡有一個稱為bi_io_vec一維陣列的成員,該陣列成員紀錄欲傳輸的資料緩衝區在記憶體何處。

【Reference】
1. Linux Device Driver, third edition
2. Linux Kernel Source 2.6.31
3. Request-based Device-mapper multipath and Dynamic load balancing
4. Understanding the Linux Kernel, Third Edition - Chapter 14. Block Device Drivers

簡介Linux Block I/O Layer (一) - Page, BIO (Block I/O) and Request 結構意義

Linux Block I/O Layer主要處理上層檔案系統的請求,進而將該請求往丟至低層的low-level device driver (例如: Linux SCSI Subsystem),下圖展示出這三層的關係,由圖中可知檔案系統驅動程式主要以page結構來描述所欲存取的資料在哪裡, 接著檔案系統驅動程式將page結構轉換bio (Block I/O),並經由submit_bio函式將該請求送至Block I/O Layer,該層主要任務便將bio結構轉換成request結構 (往後會有幾篇探討該層運作細節),並將該request結構丟至low-level device driver,以上是簡單的介紹.



【Reference】
1. Linux Device Driver, third edition
2. Linux Kernel Source 2.6.31
3. Request-based Device-mapper multipath and Dynamic load balancing

2010年3月15日 星期一

C語言malloc之sizeof使用技巧

C語言程式設計師使用結構指標時,在配置一塊記憶體時通常都會使用下列宣告描述 (粗體字):

struct abc {
     char *ptr;
     int var[20];
     struct abc *next;
};

struct abc *ptr = (struct abc *) malloc(sizeof(struct abc));


另一種寫法可以將程式碼簡化,將sizeof(struct abc)改成sizeof(*ptr),如下所示:

struct abc *ptr = malloc(sizeof(*ptr));


提供給各位參考.

2010年1月25日 星期一

Linux Kernel: container_of 巨集

Linux驅動程式裡很常看container_of巨集,其目的為何呢? 假設Linux驅動程式只知道某一結構成員的位址,該驅動程式便可使用container_of巨集,將已知某一結構成員的位址計算出該結構的起始位址,其原型如下:


#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

#define container_of(ptr, type, member) ({ \
     const typeof( ((type *)0)->member ) *__mptr = (ptr); \
     (type *)( (char *)__mptr - offsetof(type,member) );})



假設有一結構定義如下,且我們只知道phone_num成員的位址,如此便能使用該巨集計算出該結構變數的起始位址:


/* The student structure definition */
typedef struct student {
     char name[16];
     int id;
     char addr[64];
     char phone_num[16];
}STUDENT, *STUDENT_PTR;


該結構記憶體配置圖如下所示:


Figure 1. 結構記憶體配置圖

範例程式碼:

Figure 2. 範例程式碼


以此結構為範例來看containter_of巨集的兩行程式碼:

◎ const typeof( ((type *)0)->member ) *__mptr = (ptr); ==> 得到phone_num的位址 (x+84) ,請參考Figure 1跟Figure 2。
◎ (type *)( (char *)__mptr - offsetof(type,member) ); → (type *)( (char *)__mptr - ((size_t) &((type *)0)->member) ); ==> 其中((size_t) &((type *)0)->member)這段敘述代表以零為起始位址算出member這個成員的相對位址,即為phone_num成員的相對位址,也就是84 (請參考Figure 1)。所以整個敘述變成 (x+84) - 84 = x ,如此便能取得該結構變數的起始位址。


範例程式

2010年1月22日 星期五

Linux Kernel: Lookaside Cache (前瞻快取)

驅動程式往往根據使用者請求一次又一次地配置相同記憶體大小的物件。為此,Linux核心提供此需求的機制 - Lookaside cache。而此機制稱為Slab Allocator。

以SCSI驅動程式為例,當使用者要讀寫硬碟資料時,會由區塊裝置驅動程式 (Block Device Driver)向SCSI層發出請求,SCSI層將此請求轉換成SCSI command的結構 (struct scsi_cmnd),為了能快速配置與存取SCSI command,SCSI層驅動程式為每一個SCSI command結構配置lookaside cache。其用法如下:

1. 首先,必須先配置一個快取物件導向,其函式: struct kmem_cache *
kmem_cache_create (const char *name, size_t size, size_t align,
unsigned long flags, void (*ctor)(void *))


    ◎ name: 此快取物件名稱,此名稱會出現在/proc/slabinfo。
    ◎ size: 物件大小。
    ◎ align: 對齊字元數大小,通常為零。
    ◎ flag: 配置物件的方式,詳見include/linux/slab.h。
    ◎ ctor: 建構子函式位址。當核心成功地配置物件後,便會呼叫此函式。

2. 接著,呼叫kmem_cache_alloc,配置每一個物件,其原型: void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);

    ◎ cachep: kmem_cache_create回傳的位址。
    ◎ flags: 詳見kmalloc的flags參數。

3. 釋放記憶體:
void kmem_cache_free(struct kmem_cache *cachep, void *objp)
→ 釋放物件,但並未釋放快取。
void kmem_cache_destroy(struct kmem_cache *cachep) → 釋放快取。



範例 (取自Linux SCSI驅動程式):


【Reference】
1. Linux Device Driver, third edition
2. Linux Kernel Source 2.6.31