我們要將 Orange'S 安裝到硬碟上,並實現硬碟啟動。
回憶軟碟啟動的過程:
回憶軟碟啟動的過程:
- BIOS 將開機磁區讀入記憶體 0000:7C00 處
- 跳轉到 0000:7C00 處開始執行開機程式碼
- 開機程式碼從軟碟中找到 loader.bin,並將其讀入記憶體
- 跳轉到 loader.bin 開始執行
- loader.bin 從軟碟中找到 kernel.bin,並將其讀入記憶體
- 跳轉到 kernel.bin 開始執行,到此可認為啟動過程結束
- 系統執行中
第 1 步中,係由 CMOS 來決定。第 3 步和第 5 步中,
- 軟碟啟動:程式碼將在軟碟中尋找 loader.bin 和 kernel.bin
- 硬碟啟動:需要讓開機磁區程式碼從硬碟中尋找 loader.bin 並讓 loader 從硬碟中尋找 kernel.bin
故我們必須重寫 boot.asm 和 loader.asm,讓它們讀取硬碟而不是軟碟。新的檔我們取名為 hdboot.asm 和 hdldr.asm。
程式碼
include/sys/proto.h
PUBLIC int do_lseek(); // 新增
boot/hdboot.asm
; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ; hdboot.asm ; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ; Forrest Yu, 2008 ; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ org 0x7c00 ; bios always loads boot sector to 0000:7C00 jmp boot_start %include "load.inc" STACK_BASE equ 0x7C00 ; base address of stack when booting TRANS_SECT_NR equ 2 SECT_BUF_SIZE equ TRANS_SECT_NR * 512 disk_address_packet: db 0x10 ; [ 0] Packet size in bytes. db 0 ; [ 1] Reserved, must be 0. db TRANS_SECT_NR ; [ 2] Nr of blocks to transfer. db 0 ; [ 3] Reserved, must be 0. dw 0 ; [ 4] Addr of transfer - Offset dw SUPER_BLK_SEG ; [ 6] buffer. - Seg dd 0 ; [ 8] LBA. Low 32-bits. dd 0 ; [12] LBA. High 32-bits. err: mov dh, 3 ; "Error 0 " call disp_str ; display the string jmp $ boot_start: mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, STACK_BASE call clear_screen mov dh, 0 ; "Booting " call disp_str ; display the string ;; read the super block to SUPER_BLK_SEG::0 mov dword [disk_address_packet + 8], ROOT_BASE + 1 call read_sector mov ax, SUPER_BLK_SEG mov fs, ax mov dword [disk_address_packet + 4], LOADER_OFF mov dword [disk_address_packet + 6], LOADER_SEG ;; get the sector nr of `/' (ROOT_INODE), it'll be stored in eax mov eax, [fs:SB_ROOT_INODE] call get_inode ;; read `/' into ex:bx mov dword [disk_address_packet + 8], eax call read_sector ;; let's search `/' for the loader mov si, LoaderFileName push bx ; <- save .str_cmp: ;; before comparation: ;; es:bx -> dir_entry @ disk ;; ds:si -> filename we want add bx, [fs:SB_DIR_ENT_FNAME_OFF] .1: lodsb ; ds:si -> al cmp al, byte [es:bx] jz .2 jmp .different ; oops .2: ; so far so good cmp al, 0 ; both arrive at a '\0', match jz .found inc bx ; next char @ disk jmp .1 ; on and on .different: pop bx ; -> restore add bx, [fs:SB_DIR_ENT_SIZE] sub ecx, [fs:SB_DIR_ENT_SIZE] jz .not_found mov dx, SECT_BUF_SIZE cmp bx, dx jge .not_found push bx mov si, LoaderFileName jmp .str_cmp .not_found: mov dh, 2 call disp_str jmp $ .found: pop bx add bx, [fs:SB_DIR_ENT_INODE_OFF] mov eax, [es:bx] ; eax <- inode nr of loader call get_inode ; eax <- start sector nr of loader mov dword [disk_address_packet + 8], eax load_loader: call read_sector cmp ecx, SECT_BUF_SIZE jl .done sub ecx, SECT_BUF_SIZE ; bytes_left -= SECT_BUF_SIZE add word [disk_address_packet + 4], SECT_BUF_SIZE ; transfer buffer jc err add dword [disk_address_packet + 8], TRANS_SECT_NR ; LBA jmp load_loader .done: mov dh, 1 call disp_str jmp LOADER_SEG:LOADER_OFF jmp $ ;============================================================================ ;字元串 ;---------------------------------------------------------------------------- LoaderFileName db "hdldr.bin", 0 ; LOADER 之檔案名 ; 為簡化程式碼, 下面每個字元串的長度均為 MessageLength MessageLength equ 9 BootMessage: db "BootingHD"; 9字元, 不夠則用空格補齊. 序號 0 Message1 db "HD Boot "; 9字元, 不夠則用空格補齊. 序號 1 Message2 db "No LOADER"; 9字元, 不夠則用空格補齊. 序號 2 Message3 db "Error 0 "; 9字元, 不夠則用空格補齊. 序號 3 ;============================================================================ clear_screen: mov ax, 0x600 ; AH = 6, AL = 0 mov bx, 0x700 ; 黑底白字(BL = 0x7) mov cx, 0 ; 左上角: (0, 0) mov dx, 0x184f ; 右下角: (80, 50) int 0x10 ; int 0x10 ret ;---------------------------------------------------------------------------- ; 函數名: disp_str ;---------------------------------------------------------------------------- ; 作用: ; 顯示一個字元串, 函數開始時 dh 中應該是字元串序號(0-based) disp_str: mov ax, MessageLength mul dh add ax, BootMessage mov bp, ax ; `. mov ax, ds ; | ES:BP = 串位址 mov es, ax ; / mov cx, MessageLength ; CX = 串長度 mov ax, 0x1301 ; AH = 0x13, AL = 0x1 mov bx, 0x7 ; 頁號為0(BH = 0) 黑底白字(BL = 0x7) mov dl, 0 int 0x10 ; int 0x10 ret ;---------------------------------------------------------------------------- ; read_sector ;---------------------------------------------------------------------------- ; Entry: ; - fields disk_address_packet should have been filled ; before invoking the routine ; Exit: ; - es:bx -> data read ; registers changed: ; - eax, ebx, dl, si, es read_sector: xor ebx, ebx mov ah, 0x42 mov dl, 0x80 mov si, disk_address_packet int 0x13 mov ax, [disk_address_packet + 6] mov es, ax mov bx, [disk_address_packet + 4] ret ;---------------------------------------------------------------------------- ; get_inode ;---------------------------------------------------------------------------- ; Entry: ; - eax : inode nr. ; Exit: ; - eax : sector nr. ; - ecx : the_inode.i_size ; - es:ebx : inodes sector buffer ; registers changed: ; - eax, ebx, ecx, edx get_inode: dec eax ; eax <- inode_nr -1 mov bl, [fs:SB_INODE_SIZE] mul bl ; eax <- (inode_nr - 1) * INODE_SIZE mov edx, SECT_BUF_SIZE sub edx, dword [fs:SB_INODE_SIZE] cmp eax, edx jg err push eax mov ebx, [fs:SB_NR_IMAP_SECTS] mov edx, [fs:SB_NR_SMAP_SECTS] lea eax, [ebx+edx+ROOT_BASE+2] mov dword [disk_address_packet + 8], eax call read_sector pop eax ; [es:ebx+eax] -> the inode mov edx, dword [fs:SB_INODE_ISIZE_OFF] add edx, ebx add edx, eax ; [es:edx] -> the_inode.i_size mov ecx, [es:edx] ; ecx <- the_inode.i_size ; es:[ebx+eax] -> the_inode.i_start_sect add ax, word [fs:SB_INODE_START_OFF] add bx, ax mov eax, [es:bx] add eax, ROOT_BASE ; eax <- the_inode.i_start_sect ret times 510-($-$$) db 0 ; 填充剩下的空間,使生成的二進制程式碼恰好為512字元 dw 0xaa55 ; 結束標誌
boot/hdldr.asm
; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
; loader.asm
; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
; Forrest Yu, 2005
; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
org 0100h
jmp LABEL_START ; Start
%include "load.inc"
%include "pm.inc"
TRANS_SECT_NR equ 2
SECT_BUF_SIZE equ TRANS_SECT_NR * 512
disk_address_packet: db 0x10 ; [ 0] Packet size in bytes. Must be 0x10 or greater.
db 0 ; [ 1] Reserved, must be 0.
sect_cnt: db TRANS_SECT_NR ; [ 2] Number of blocks to transfer.
db 0 ; [ 3] Reserved, must be 0.
dw KERNEL_FILE_OFF ; [ 4] Address of transfer buffer. Offset
dw KERNEL_FILE_SEG ; [ 6] Seg
lba_addr: dd 0 ; [ 8] Starting LBA address. Low 32-bits.
dd 0 ; [12] Starting LBA address. High 32-bits.
; GDT ------------------------------------------------------------------------------------------------------------------------------------------------------------
; 段基址 段界限 , 属性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_FLAT_C: Descriptor 0, 0fffffh, DA_CR | DA_32 | DA_LIMIT_4K ; 0 ~ 4G
LABEL_DESC_FLAT_RW: Descriptor 0, 0fffffh, DA_DRW | DA_32 | DA_LIMIT_4K ; 0 ~ 4G
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW | DA_DPL3 ; 显存首位址
; GDT ------------------------------------------------------------------------------------------------------------------------------------------------------------
GdtLen equ $ - LABEL_GDT
GdtPtr dw GdtLen - 1 ; 段界限
dd LOADER_PHY_ADDR + LABEL_GDT ; 基位址 (让基位址八字节对齐将起到优化速度之效果,目前懒得改)
; The GDT is not a segment itself; instead, it is a data structure in linear address space.
; The base linear address and limit of the GDT must be loaded into the GDTR register. -- IA-32 Software Developer’s Manual, Vol.3A
; GDT 选择子 ----------------------------------------------------------------------------------
SelectorFlatC equ LABEL_DESC_FLAT_C - LABEL_GDT
SelectorFlatRW equ LABEL_DESC_FLAT_RW - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT + SA_RPL3
; GDT 选择子 ----------------------------------------------------------------------------------
BaseOfStack equ 0100h
err:
mov dh, 5 ; "Error 0 "
call real_mode_disp_str ; display the string
jmp $
LABEL_START: ; <--- 从这里开始 *************
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, BaseOfStack
mov dh, 0 ; "Loading "
call real_mode_disp_str ; 显示字元串
; 得到内存数
mov ebx, 0 ; ebx = 后续值, 开始时需为 0
mov di, _MemChkBuf ; es:di 指向一个位址范围描述符结构(Address Range Descriptor Structure)
.MemChkLoop:
mov eax, 0E820h ; eax = 0000E820h
mov ecx, 20 ; ecx = 位址范围描述符结构的大小
mov edx, 0534D4150h ; edx = 'SMAP'
int 15h ; int 15h
jc .MemChkFail
add di, 20
inc dword [_dwMCRNumber] ; dwMCRNumber = ARDS 的个数
cmp ebx, 0
jne .MemChkLoop
jmp .MemChkOK
.MemChkFail:
mov dword [_dwMCRNumber], 0
.MemChkOK:
;; get the sector nr of `/' (ROOT_INODE), it'll be stored in eax
mov eax, [fs:SB_ROOT_INODE] ; fs -> super_block (see hdboot.asm)
call get_inode
;; read `/' into es:bx
mov dword [disk_address_packet + 8], eax
call read_sector
;; let's search `/' for the kernel
mov si, KernelFileName
push bx ; <- save
.str_cmp:
;; before comparation:
;; es:bx -> dir_entry @ disk
;; ds:si -> filename we want
add bx, [fs:SB_DIR_ENT_FNAME_OFF]
.1:
lodsb ; ds:si -> al
cmp al, byte [es:bx]
jz .2
jmp .different ; oops
.2: ; so far so good
cmp al, 0 ; both arrive at a '\0', match
jz .found
inc bx ; next char @ disk
jmp .1 ; on and on
.different:
pop bx ; -> restore
add bx, [fs:SB_DIR_ENT_SIZE]
sub ecx, [fs:SB_DIR_ENT_SIZE]
jz .not_found
push bx
mov si, KernelFileName
jmp .str_cmp
.not_found:
mov dh, 3
call real_mode_disp_str
jmp $
.found:
pop bx
add bx, [fs:SB_DIR_ENT_INODE_OFF]
mov eax, [es:bx] ; eax <- inode nr of kernel
call get_inode ; eax <- start sector nr of kernel
mov dword [disk_address_packet + 8], eax
load_kernel:
call read_sector
cmp ecx, SECT_BUF_SIZE
jl .done
sub ecx, SECT_BUF_SIZE ; bytes_left -= SECT_BUF_SIZE
add word [disk_address_packet + 4], SECT_BUF_SIZE ; transfer buffer
jc .1
jmp .2
.1:
add word [disk_address_packet + 6], 1000h
.2:
add dword [disk_address_packet + 8], TRANS_SECT_NR ; LBA
jmp load_kernel
.done:
mov dh, 2
call real_mode_disp_str
; 下面准备跳入保护模式 -------------------------------------------
; 加载 GDTR
lgdt [GdtPtr]
; 关中断
cli
; 打开位址线A20
in al, 92h
or al, 00000010b
out 92h, al
; 准备切换到保护模式
mov eax, cr0
or eax, 1
mov cr0, eax
; 真正进入保护模式
jmp dword SelectorFlatC:(LOADER_PHY_ADDR+LABEL_PM_START)
jmp $ ; never arrive here
;============================================================================
;变量
;----------------------------------------------------------------------------
wSectorNo dw 0 ; 要读取的扇区号
bOdd db 0 ; 奇数还是偶数
dwKernelSize dd 0 ; KERNEL.BIN 檔案大小
;============================================================================
;字元串
;----------------------------------------------------------------------------
KernelFileName db "kernel.bin", 0 ; KERNEL.BIN 之檔案名
; 为简化代码, 下面每个字元串的长度均为 MessageLength
MessageLength equ 9
LoadMessage: db "Loading "
Message1 db " "
Message2 db "in HD LDR"
Message3 db "No KERNEL"
Message4 db "Too Large"
Message5 db "Error 0 "
;============================================================================
;============================================================================
clear_screen:
mov ax, 0x600 ; AH = 6, AL = 0
mov bx, 0x700 ; 黑底白字(BL = 0x7)
mov cx, 0 ; 左上角: (0, 0)
mov dx, 0x184f ; 右下角: (80, 50)
int 0x10 ; int 0x10
ret
;----------------------------------------------------------------------------
; 函数名: disp_str
;----------------------------------------------------------------------------
; 作用:
; 显示一个字元串, 函数开始时 dh 中应该是字元串序号(0-based)
real_mode_disp_str:
mov ax, MessageLength
mul dh
add ax, LoadMessage
mov bp, ax ; ┓
mov ax, ds ; ┣ ES:BP = 串位址
mov es, ax ; ┛
mov cx, MessageLength ; CX = 串长度
mov ax, 0x1301 ; AH = 0x13, AL = 0x1
mov bx, 0x7 ; 页号为0(BH = 0) 黑底白字(BL = 0x7)
mov dl, 0
int 0x10 ; int 0x10
ret
;----------------------------------------------------------------------------
; read_sector
;----------------------------------------------------------------------------
; before:
; - fields disk_address_packet should have been filled
; before invoking the routine
; after:
; - es:bx -> data read
; registers changed:
; - eax, ebx, dl, si, es
read_sector:
xor ebx, ebx
;mov dword [disk_address_packet + 8], eax
mov dword [disk_address_packet + 12], 0
mov ah, 0x42
mov dl, 0x80
mov si, disk_address_packet
int 0x13
mov ax, [disk_address_packet + 6]
mov es, ax
mov bx, [disk_address_packet + 4]
ret
;----------------------------------------------------------------------------
; get_inode
;----------------------------------------------------------------------------
; before:
; - eax : inode nr.
; after:
; - eax : sector nr.
; - ecx : the_inode.i_size
; - es:ebx : inodes sector buffer
; registers changed:
; - eax, ebx, ecx, edx
get_inode:
dec eax ; eax <- inode_nr -1
mov bl, [fs:SB_INODE_SIZE]
mul bl ; eax <- (inode_nr - 1) * INODE_SIZE
mov edx, SECT_BUF_SIZE
sub edx, dword [fs:SB_INODE_SIZE]
cmp eax, edx
jg err
push eax
mov ebx, [fs:SB_NR_IMAP_SECTS]
mov edx, [fs:SB_NR_SMAP_SECTS]
lea eax, [ebx+edx+ROOT_BASE+2]
mov dword [disk_address_packet + 8], eax
call read_sector
pop eax ; [es:ebx+eax] -> the inode
mov edx, dword [fs:SB_INODE_ISIZE_OFF]
add edx, ebx
add edx, eax ; [es:edx] -> the_inode.i_size
mov ecx, [es:edx] ; ecx <- the_inode.i_size
add ax, word [fs:SB_INODE_START_OFF]; es:[ebx+eax] -> the_inode.i_start_sect
add bx, ax
mov eax, [es:bx]
add eax, ROOT_BASE ; eax <- the_inode.i_start_sect
ret
; 从此以后的代码在保护模式下执行 ----------------------------------------------------
; 32 位代码段. 由实模式跳入 ---------------------------------------------------------
[SECTION .s32]
ALIGN 32
[BITS 32]
LABEL_PM_START:
mov ax, SelectorVideo
mov gs, ax
mov ax, SelectorFlatRW
mov ds, ax
mov es, ax
mov fs, ax
mov ss, ax
mov esp, TopOfStack
call DispMemInfo
;;; call DispReturn
;;; call DispHDInfo ; int 13h 读出的硬盘 geometry 好像有点不对头,不知道为什么,干脆不管它了
call SetupPaging
;mov ah, 0Fh ; 0000: 黑底 1111: 白字
;mov al, 'P'
;mov [gs:((80 * 0 + 39) * 2)], ax ; 螢幕第 0 行, 第 39 列。
call InitKernel
;jmp $
mov dword [BOOT_PARAM_ADDR], BOOT_PARAM_MAGIC ; BootParam[0] = BootParamMagic;
mov eax, [dwMemSize] ;
mov [BOOT_PARAM_ADDR + 4], eax ; BootParam[1] = MemSize;
mov eax, KERNEL_FILE_SEG
shl eax, 4
add eax, KERNEL_FILE_OFF
mov [BOOT_PARAM_ADDR + 8], eax ; BootParam[2] = KernelFilePhyAddr;
;***************************************************************
jmp SelectorFlatC:KRNL_ENT_PT_PHY_ADDR ; 正式进入内核 *
;***************************************************************
; 記憶體看上去是這樣的:
; ┃ ┃
; ┃ . ┃
; ┃ . ┃
; ┃ . ┃
; ┣━━━━━━━━━━━━━━━━━━┫
; ┃■■■■■■■■■■■■■■■■■■┃
; ┃■■■■■■Page Tables■■■■■■┃
; ┃■■■■■(大小由LOADER決定)■■■■┃
; 00101000h ┃■■■■■■■■■■■■■■■■■■┃ PAGE_TBL_BASE
; ┣━━━━━━━━━━━━━━━━━━┫
; ┃■■■■■■■■■■■■■■■■■■┃
; 00100000h ┃■■■■Page Directory Table■■■■┃ PAGE_DIR_BASE <- 1M
; ┣━━━━━━━━━━━━━━━━━━┫
; ┃□□□□□□□□□□□□□□□□□□┃
; F0000h ┃□□□□□□□System ROM□□□□□□┃
; ┣━━━━━━━━━━━━━━━━━━┫
; ┃□□□□□□□□□□□□□□□□□□┃
; E0000h ┃□□□□Expansion of system ROM □□┃
; ┣━━━━━━━━━━━━━━━━━━┫
; ┃□□□□□□□□□□□□□□□□□□┃
; C0000h ┃□□□Reserved for ROM expansion□□┃
; ┣━━━━━━━━━━━━━━━━━━┫
; ┃□□□□□□□□□□□□□□□□□□┃ B8000h ← gs
; A0000h ┃□□□Display adapter reserved□□□┃
; ┣━━━━━━━━━━━━━━━━━━┫
; ┃□□□□□□□□□□□□□□□□□□┃
; 9FC00h ┃□□extended BIOS data area (EBDA)□┃
; ┣━━━━━━━━━━━━━━━━━━┫
; ┃■■■■■■■■■■■■■■■■■■┃
; 90000h ┃■■■■■■■LOADER.BIN■■■■■■┃ somewhere in LOADER ← esp
; ┣━━━━━━━━━━━━━━━━━━┫
; ┃■■■■■■■■■■■■■■■■■■┃
; 80000h ┃■■■■■■■KERNEL.BIN■■■■■■┃
; ┣━━━━━━━━━━━━━━━━━━┫
; ┃■■■■■■■■■■■■■■■■■■┃
; 30000h ┃■■■■■■■■KERNEL■■■■■■■┃ 30400h ← KERNEL 入口 (KRNL_ENT_PT_PHY_ADDR)
; ┣━━━━━━━━━━━━━━━━━━┫
; ┃ ┃
; 7E00h ┃ F R E E ┃
; ┣━━━━━━━━━━━━━━━━━━┫
; ┃■■■■■■■■■■■■■■■■■■┃
; 7C00h ┃■■■■■■BOOT SECTOR■■■■■■┃
; ┣━━━━━━━━━━━━━━━━━━┫
; ┃ ┃
; 500h ┃ F R E E ┃
; ┣━━━━━━━━━━━━━━━━━━┫
; ┃□□□□□□□□□□□□□□□□□□┃
; 400h ┃□□□□ROM BIOS parameter area □□┃
; ┣━━━━━━━━━━━━━━━━━━┫
; ┃◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇┃
; 0h ┃◇◇◇◇◇◇Int Vectors◇◇◇◇◇◇┃
; ┗━━━━━━━━━━━━━━━━━━┛ ← cs, ds, es, fs, ss
;
;
; ┏━━━┓ ┏━━━┓
; ┃■■■┃ 我們使用 ┃□□□┃ 不能使用的記憶體
; ┗━━━┛ ┗━━━┛
; ┏━━━┓ ┏━━━┓
; ┃ ┃ 未使用空間 ┃◇◇◇┃ 可以覆蓋的記憶體
; ┗━━━┛ ┗━━━┛
;
; 注:KERNEL 的位置實際上是很靈活的,可以通過同時改變 LOAD.INC 中的 KRNL_ENT_PT_PHY_ADDR 和 MAKEFILE 中參數 -Ttext 的值來改變。
; 比如,如果把 KRNL_ENT_PT_PHY_ADDR 和 -Ttext 的值都改為 0x400400,則 KERNEL 就會被加載到記憶體 0x400000(4M) 處,入口在 0x400400。
;
; ------------------------------------------------------------------------
; 顯示 AL 中的數字
; ------------------------------------------------------------------------
DispAL:
push ecx
push edx
push edi
mov edi, [dwDispPos]
mov ah, 0Fh ; 0000b: 黑底 1111b: 白字
mov dl, al
shr al, 4
mov ecx, 2
.begin:
and al, 01111b
cmp al, 9
ja .1
add al, '0'
jmp .2
.1:
sub al, 0Ah
add al, 'A'
.2:
mov [gs:edi], ax
add edi, 2
mov al, dl
loop .begin
;add edi, 2
mov [dwDispPos], edi
pop edi
pop edx
pop ecx
ret
; DispAL 結束-------------------------------------------------------------
; ------------------------------------------------------------------------
; 顯示一個整形數
; ------------------------------------------------------------------------
DispInt:
mov eax, [esp + 4]
shr eax, 24
call DispAL
mov eax, [esp + 4]
shr eax, 16
call DispAL
mov eax, [esp + 4]
shr eax, 8
call DispAL
mov eax, [esp + 4]
call DispAL
mov ah, 07h ; 0000b: 黑底 0111b: 灰字
mov al, 'h'
push edi
mov edi, [dwDispPos]
mov [gs:edi], ax
add edi, 4
mov [dwDispPos], edi
pop edi
ret
; DispInt 結束------------------------------------------------------------
; ------------------------------------------------------------------------
; 顯示一個字元串
; ------------------------------------------------------------------------
DispStr:
push ebp
mov ebp, esp
push ebx
push esi
push edi
mov esi, [ebp + 8] ; pszInfo
mov edi, [dwDispPos]
mov ah, 0Fh
.1:
lodsb
test al, al
jz .2
cmp al, 0Ah ; 是ENTER嗎?
jnz .3
push eax
mov eax, edi
mov bl, 160
div bl
and eax, 0FFh
inc eax
mov bl, 160
mul bl
mov edi, eax
pop eax
jmp .1
.3:
mov [gs:edi], ax
add edi, 2
jmp .1
.2:
mov [dwDispPos], edi
pop edi
pop esi
pop ebx
pop ebp
ret
; DispStr 結束------------------------------------------------------------
; ------------------------------------------------------------------------
; 換行
; ------------------------------------------------------------------------
DispReturn:
push szReturn
call DispStr ;printf("\n");
add esp, 4
ret
; DispReturn 結束---------------------------------------------------------
; ------------------------------------------------------------------------
; 記憶體拷貝,仿 memcpy
; ------------------------------------------------------------------------
; void* MemCpy(void* es:pDest, void* ds:pSrc, int iSize);
; ------------------------------------------------------------------------
MemCpy:
push ebp
mov ebp, esp
push esi
push edi
push ecx
mov edi, [ebp + 8] ; Destination
mov esi, [ebp + 12] ; Source
mov ecx, [ebp + 16] ; Counter
.1:
cmp ecx, 0 ; 判斷計數器
jz .2 ; 計數器為零時跳出
mov al, [ds:esi] ; ┓
inc esi ; ┃
; ┣ 逐字元移動
mov byte [es:edi], al ; ┃
inc edi ; ┛
dec ecx ; 計數器減一
jmp .1 ; 循環
.2:
mov eax, [ebp + 8] ; 返回值
pop ecx
pop edi
pop esi
mov esp, ebp
pop ebp
ret ; 函數結束,返回
; MemCpy 結束-------------------------------------------------------------
; 顯示記憶體訊息 --------------------------------------------------------------
DispMemInfo:
push esi
push edi
push ecx
push szMemChkTitle
call DispStr
add esp, 4
mov esi, MemChkBuf
mov ecx, [dwMCRNumber] ;for(int i=0;i<[MCRNumber];i++) // 每次得到一個ARDS(Address Range Descriptor Structure)結構
.loop: ;{
mov edx, 5 ; for(int j=0;j<5;j++) // 每次得到一個ARDS中的成員,共5個成員
mov edi, ARDStruct ; { // 依次顯示:BaseAddrLow,BaseAddrHigh,LengthLow,LengthHigh,Type
.1: ;
push dword [esi] ;
call DispInt ; DispInt(MemChkBuf[j*4]); // 顯示一個成員
pop eax ;
stosd ; ARDStruct[j*4] = MemChkBuf[j*4];
add esi, 4 ;
dec edx ;
cmp edx, 0 ;
jnz .1 ; }
call DispReturn ; printf("\n");
cmp dword [dwType], 1 ; if(Type == AddressRangeMemory) // AddressRangeMemory : 1, AddressRangeReserved : 2
jne .2 ; {
mov eax, [dwBaseAddrLow] ;
add eax, [dwLengthLow] ;
cmp eax, [dwMemSize] ; if(BaseAddrLow + LengthLow > MemSize)
jb .2 ;
mov [dwMemSize], eax ; MemSize = BaseAddrLow + LengthLow;
.2: ; }
loop .loop ;}
;
call DispReturn ;printf("\n");
push szRAMSize ;
call DispStr ;printf("RAM size:");
add esp, 4 ;
;
push dword [dwMemSize] ;
call DispInt ;DispInt(MemSize);
add esp, 4 ;
pop ecx
pop edi
pop esi
ret
; ---------------------------------------------------------------------------
;;; ; 顯示記憶體訊息 --------------------------------------------------------------
;;; DispHDInfo:
;;; push eax
;;; cmp dword [dwNrHead], 0FFFFh
;;; je .nohd
;;; push szCylinder
;;; call DispStr ; printf("C:");
;;; add esp, 4
;;; push dword [dwNrCylinder] ; NR Cylinder
;;; call DispInt
;;; pop eax
;;; push szHead
;;; call DispStr ; printf(" H:");
;;; add esp, 4
;;; push dword [dwNrHead] ; NR Head
;;; call DispInt
;;; pop eax
;;; push szSector
;;; call DispStr ; printf(" S:");
;;; add esp, 4
;;; push dword [dwNrSector] ; NR Sector
;;; call DispInt
;;; pop eax
;;; jmp .hdinfo_finish
;;; .nohd:
;;; push szNOHD
;;; call DispStr ; printf("No hard drive. System halt.");
;;; add esp, 4
;;; jmp $ ; 沒有硬碟,死在這裡
;;; .hdinfo_finish:
;;; call DispReturn
;;; pop eax
;;; ret
;;; ; ---------------------------------------------------------------------------
; 啟動分頁機制 --------------------------------------------------------------
SetupPaging:
; 根據記憶體大小計算應初始化多少PDE以及多少頁表
xor edx, edx
mov eax, [dwMemSize]
mov ebx, 400000h ; 400000h = 4M = 4096 * 1024, 一個頁表對應的記憶體大小
div ebx
mov ecx, eax ; 此時 ecx 為頁表的個數,也即 PDE 應該的個數
test edx, edx
jz .no_remainder
inc ecx ; 如果餘數不為 0 就需增加一個頁表
.no_remainder:
push ecx ; 暫存頁表個數
; 為簡化處理, 所有線性位址對應相等的物理位址. 並且不考慮記憶體空洞.
; 首先初始化頁目錄
mov ax, SelectorFlatRW
mov es, ax
mov edi, PAGE_DIR_BASE ; 此段首位址為 PAGE_DIR_BASE
xor eax, eax
mov eax, PAGE_TBL_BASE | PG_P | PG_USU | PG_RWW
.1:
stosd
add eax, 4096 ; 為了簡化, 所有頁表在記憶體中是連續的.
loop .1
; 再初始化所有頁表
pop eax ; 頁表個數
mov ebx, 1024 ; 每個頁表 1024 個 PTE
mul ebx
mov ecx, eax ; PTE個數 = 頁表個數 * 1024
mov edi, PAGE_TBL_BASE ; 此段首位址為 PAGE_TBL_BASE
xor eax, eax
mov eax, PG_P | PG_USU | PG_RWW
.2:
stosd
add eax, 4096 ; 每一頁指向 4K 的空間
loop .2
mov eax, PAGE_DIR_BASE
mov cr3, eax
mov eax, cr0
or eax, 80000000h
mov cr0, eax
jmp short .3
.3:
nop
ret
; 分頁機制啟動完畢 ----------------------------------------------------------
; InitKernel ---------------------------------------------------------------------------------
; 將 KERNEL.BIN 的內容經過整理對齊後放到新的位置
; --------------------------------------------------------------------------------------------
InitKernel: ; 遍歷每一個 Program Header,根據 Program Header 中的訊息來確定把什麼放進記憶體,放到什麼位置,以及放多少。
xor esi, esi
mov cx, word [KERNEL_FILE_PHY_ADDR + 2Ch] ; ┓ ecx <- pELFHdr->e_phnum
movzx ecx, cx ; ┛
mov esi, [KERNEL_FILE_PHY_ADDR + 1Ch] ; esi <- pELFHdr->e_phoff
add esi, KERNEL_FILE_PHY_ADDR ; esi <- OffsetOfKernel + pELFHdr->e_phoff
.Begin:
mov eax, [esi + 0]
cmp eax, 0 ; PT_NULL
jz .NoAction
push dword [esi + 010h] ; size ┓
mov eax, [esi + 04h] ; ┃
add eax, KERNEL_FILE_PHY_ADDR ; ┣ ::memcpy( (void*)(pPHdr->p_vaddr),
push eax ; src ┃ uchCode + pPHdr->p_offset,
push dword [esi + 08h] ; dst ┃ pPHdr->p_filesz;
call MemCpy ; ┃
add esp, 12 ; ┛
.NoAction:
add esi, 020h ; esi += pELFHdr->e_phentsize
dec ecx
jnz .Begin
ret
; InitKernel ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
; SECTION .data1 之開始 ---------------------------------------------------------------------------------------------
[SECTION .data1]
ALIGN 32
LABEL_DATA:
; 實模式下使用這些符號
; 字元串
_szMemChkTitle: db "BaseAddrL BaseAddrH LengthLow LengthHigh Type", 0Ah, 0
_szRAMSize: db "RAM size: ", 0
;;; _szCylinder db "HD Info : C=", 0
;;; _szHead db " H=", 0
;;; _szSector db " S=", 0
;;; _szNOHD db "No hard drive. System halt.", 0
_szReturn: db 0Ah, 0
;; 變量
;;; _dwNrCylinder dd 0
;;; _dwNrHead dd 0
;;; _dwNrSector dd 0
_dwMCRNumber: dd 0 ; Memory Check Result
_dwDispPos: dd (80 * 7 + 0) * 2 ; 螢幕第 7 行, 第 0 列。
_dwMemSize: dd 0
_ARDStruct: ; Address Range Descriptor Structure
_dwBaseAddrLow: dd 0
_dwBaseAddrHigh: dd 0
_dwLengthLow: dd 0
_dwLengthHigh: dd 0
_dwType: dd 0
_MemChkBuf: times 256 db 0
;
;; 保護模式下使用這些符號
szMemChkTitle equ LOADER_PHY_ADDR + _szMemChkTitle
szRAMSize equ LOADER_PHY_ADDR + _szRAMSize
;;; szCylinder equ LOADER_PHY_ADDR + _szCylinder
;;; szHead equ LOADER_PHY_ADDR + _szHead
;;; szSector equ LOADER_PHY_ADDR + _szSector
;;; szNOHD equ LOADER_PHY_ADDR + _szNOHD
szReturn equ LOADER_PHY_ADDR + _szReturn
;;; dwNrCylinder equ LOADER_PHY_ADDR + _dwNrCylinder
;;; dwNrHead equ LOADER_PHY_ADDR + _dwNrHead
;;; dwNrSector equ LOADER_PHY_ADDR + _dwNrSector
dwDispPos equ LOADER_PHY_ADDR + _dwDispPos
dwMemSize equ LOADER_PHY_ADDR + _dwMemSize
dwMCRNumber equ LOADER_PHY_ADDR + _dwMCRNumber
ARDStruct equ LOADER_PHY_ADDR + _ARDStruct
dwBaseAddrLow equ LOADER_PHY_ADDR + _dwBaseAddrLow
dwBaseAddrHigh equ LOADER_PHY_ADDR + _dwBaseAddrHigh
dwLengthLow equ LOADER_PHY_ADDR + _dwLengthLow
dwLengthHigh equ LOADER_PHY_ADDR + _dwLengthHigh
dwType equ LOADER_PHY_ADDR + _dwType
MemChkBuf equ LOADER_PHY_ADDR + _MemChkBuf
; 堆疊就在資料段的末尾
StackSpace: times 1000h db 0
TopOfStack equ LOADER_PHY_ADDR + $ ; 堆疊頂
; SECTION .data1 之結束 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
command/Makefile
# commands/Makefile
#ENTRYPOINT = 0x1000
HD = ../80m.img
ASM = nasm
DASM = objdump
CC = gcc
LD = ld
ASMFLAGS = -I ../include/ -f elf
CFLAGS = -I ../include/ -m32 -c -fno-builtin -fno-stack-protector -Wall
LDFLAGS = -Ttext 0x1000 -s -melf_i386
DASMFLAGS = -D
LIB = ../lib/orangescrt.a
BIN = echo pwd
# All Phony Targets
.PHONY : everything final clean realclean disasm all install
# Default starting position
everything : $(BIN)
install : all clean
cp ../boot/hdldr.bin ./ -v
cp ../kernel.bin ./ -v
tar vcf inst.tar kernel.bin $(BIN) hdldr.bin
dd if=inst.tar of=$(HD) seek=`echo "obase=10;ibase=16;(\`egrep -e '^ROOT_BASE' ../boot/include/load.inc | sed -e 's/.*0x//g'\`+\`egrep -e '#define[[:space:]]*INSTALL_START_SECT' ../include/sys/config.h | sed -e 's/.*0x//g'\`)*200" | bc` bs=1 count=`ls -l inst.tar | awk -F " " '{print $$5}'` conv=notrunc
all : realclean everything
final : all clean
clean :
rm -f *.o
realclean :
rm -f $(BIN) *.o
kernel.bin :
cp ../kernel.bin ./
start.o : start.asm
$(ASM) $(ASMFLAGS) -o $@ $<
echo.o: echo.c ../include/type.h ../include/stdio.h
$(CC) $(CFLAGS) -o $@ $<
echo : echo.o start.o $(LIB)
$(LD) $(LDFLAGS) -o $@ $?
pwd.o: pwd.c ../include/type.h ../include/stdio.h
$(CC) $(CFLAGS) -o $@ $<
pwd : pwd.o start.o $(LIB)
$(LD) $(LDFLAGS) -o $@ $?
Makefile
######################### # Makefile for Orange'S # ######################### # Entry point of Orange'S # It must have the same value with 'KernelEntryPointPhyAddr' in load.inc! ENTRYPOINT = 0x1000 FD = a.img HD = 80m.img # Programs, flags, etc. ASM = nasm DASM = objdump CC = gcc LD = ld ASMBFLAGS = -I boot/include/ ASMKFLAGS = -I include/ -I include/sys/ -f elf CFLAGS = -I include/ -I include/sys/ -m32 -c -fno-builtin -fno-stack-protector LDFLAGS = -s -melf_i386 -Ttext $(ENTRYPOINT) -Map krnl.map DASMFLAGS = -D ARFLAGS = rcs # This Program ORANGESBOOT = boot/boot.bin boot/hdboot.bin boot/loader.bin boot/hdldr.bin ORANGESKERNEL = kernel.bin LIB = lib/orangescrt.a OBJS = kernel/kernel.o kernel/start.o kernel/main.o\ kernel/clock.o kernel/keyboard.o kernel/tty.o kernel/console.o\ kernel/i8259.o kernel/global.o kernel/protect.o kernel/proc.o\ kernel/systask.o kernel/hd.o\ kernel/kliba.o kernel/klib.o\ lib/syslog.o\ mm/main.o mm/forkexit.o mm/exec.o\ fs/main.o fs/open.o fs/misc.o fs/read_write.o\ fs/link.o\ fs/disklog.o LOBJS = lib/syscall.o\ lib/printf.o lib/vsprintf.o\ lib/string.o lib/misc.o\ lib/open.o lib/read.o lib/write.o lib/close.o lib/unlink.o\ lib/getpid.o lib/stat.o\ lib/lseek.o\ lib/fork.o lib/exit.o lib/wait.o lib/exec.o DASMOUTPUT = kernel.bin.asm # All Phony Targets .PHONY : everything final image clean realclean disasm all buildimg # Default starting position nop : @echo "why not \`make image' huh? :)" everything : $(ORANGESBOOT) $(ORANGESKERNEL) all : realclean everything image : realclean everything clean buildimg app vdi clean : rm -f $(OBJS) $(LOBJS) cd ./command/; make clean realclean : rm -f $(OBJS) $(LOBJS) $(LIB) $(ORANGESBOOT) $(ORANGESKERNEL) cd ./command/; make realclean disasm : $(DASM) $(DASMFLAGS) $(ORANGESKERNEL) > $(DASMOUTPUT) # We assume that "a.img" exists in current folder buildimg : dd if=boot/boot.bin of=$(FD) bs=512 count=1 conv=notrunc dd if=boot/hdboot.bin of=$(HD) bs=1 count=446 conv=notrunc dd if=boot/hdboot.bin of=$(HD) seek=510 skip=510 bs=1 count=2 conv=notrunc #dd if=boot/hdboot.bin of=$(HD) seek=`echo "obase=10;ibase=16;\`egrep -e '^ROOT_BASE' boot/include/load.inc | sed -e 's/.*0x//g'\`*200" | bc` bs=1 count=446 conv=notrunc #dd if=boot/hdboot.bin of=$(HD) seek=`echo "obase=10;ibase=16;\`egrep -e '^ROOT_BASE' boot/include/load.inc | sed -e 's/.*0x//g'\`*200+1FE" | bc` skip=510 bs=1 count=2 conv=notrunc mkdir tmp sleep 1 sudo mount -o loop a.img tmp sleep 1 sudo cp -fv boot/loader.bin tmp sudo cp -fv kernel.bin tmp sleep 1 sudo umount tmp sleep 1 rmdir tmp app: cd ./command/; make install vdi: rm 80m.vdi VBoxManage convertfromraw -format VDI 80m.img 80m.vdi VBoxManage internalcommands sethduuid 80m.vdi ccea0f68-01ea-4b9e-8160-e43ef528ba6e debug: rm t.img VBoxManage clonehd -format RAW ccea0f68-01ea-4b9e-8160-e43ef528ba6e t.img xxd -u -a -g 1 -c 16 -s 0xAD9C00 -l 20000 t.img boot/boot.bin : boot/boot.asm boot/include/load.inc boot/include/fat12hdr.inc $(ASM) $(ASMBFLAGS) -o $@ $< boot/hdboot.bin : boot/hdboot.asm boot/include/load.inc boot/include/fat12hdr.inc $(ASM) $(ASMBFLAGS) -o $@ $< boot/loader.bin : boot/loader.asm boot/include/load.inc boot/include/fat12hdr.inc boot/include/pm.inc $(ASM) $(ASMBFLAGS) -o $@ $< boot/hdldr.bin : boot/hdldr.asm boot/include/load.inc boot/include/fat12hdr.inc boot/include/pm.inc $(ASM) $(ASMBFLAGS) -o $@ $< $(ORANGESKERNEL) : $(OBJS) $(LIB) $(LD) $(LDFLAGS) -o $(ORANGESKERNEL) $^ $(LIB) : $(LOBJS) $(AR) $(ARFLAGS) $@ $^ kernel/kernel.o : kernel/kernel.asm $(ASM) $(ASMKFLAGS) -o $@ $< lib/syscall.o : lib/syscall.asm $(ASM) $(ASMKFLAGS) -o $@ $< kernel/start.o: kernel/start.c $(CC) $(CFLAGS) -o $@ $< kernel/main.o: kernel/main.c $(CC) $(CFLAGS) -o $@ $< kernel/clock.o: kernel/clock.c $(CC) $(CFLAGS) -o $@ $< kernel/keyboard.o: kernel/keyboard.c $(CC) $(CFLAGS) -o $@ $< kernel/tty.o: kernel/tty.c $(CC) $(CFLAGS) -o $@ $< kernel/console.o: kernel/console.c $(CC) $(CFLAGS) -o $@ $< kernel/i8259.o: kernel/i8259.c $(CC) $(CFLAGS) -o $@ $< kernel/global.o: kernel/global.c $(CC) $(CFLAGS) -o $@ $< kernel/protect.o: kernel/protect.c $(CC) $(CFLAGS) -o $@ $< kernel/proc.o: kernel/proc.c $(CC) $(CFLAGS) -o $@ $< lib/printf.o: lib/printf.c $(CC) $(CFLAGS) -o $@ $< lib/vsprintf.o: lib/vsprintf.c $(CC) $(CFLAGS) -o $@ $< kernel/systask.o: kernel/systask.c $(CC) $(CFLAGS) -o $@ $< kernel/hd.o: kernel/hd.c $(CC) $(CFLAGS) -o $@ $< kernel/klib.o: kernel/klib.c $(CC) $(CFLAGS) -o $@ $< lib/misc.o: lib/misc.c $(CC) $(CFLAGS) -o $@ $< kernel/kliba.o : kernel/kliba.asm $(ASM) $(ASMKFLAGS) -o $@ $< lib/string.o : lib/string.asm $(ASM) $(ASMKFLAGS) -o $@ $< lib/open.o: lib/open.c $(CC) $(CFLAGS) -o $@ $< lib/read.o: lib/read.c $(CC) $(CFLAGS) -o $@ $< lib/write.o: lib/write.c $(CC) $(CFLAGS) -o $@ $< lib/close.o: lib/close.c $(CC) $(CFLAGS) -o $@ $< lib/unlink.o: lib/unlink.c $(CC) $(CFLAGS) -o $@ $< lib/getpid.o: lib/getpid.c $(CC) $(CFLAGS) -o $@ $< lib/syslog.o: lib/syslog.c $(CC) $(CFLAGS) -o $@ $< lib/fork.o: lib/fork.c $(CC) $(CFLAGS) -o $@ $< lib/exit.o: lib/exit.c $(CC) $(CFLAGS) -o $@ $< lib/wait.o: lib/wait.c $(CC) $(CFLAGS) -o $@ $< lib/exec.o: lib/exec.c $(CC) $(CFLAGS) -o $@ $< lib/stat.o: lib/stat.c $(CC) $(CFLAGS) -o $@ $< lib/lseek.o: lib/lseek.c $(CC) $(CFLAGS) -o $@ $< mm/main.o: mm/main.c $(CC) $(CFLAGS) -o $@ $< mm/forkexit.o: mm/forkexit.c $(CC) $(CFLAGS) -o $@ $< mm/exec.o: mm/exec.c $(CC) $(CFLAGS) -o $@ $< fs/main.o: fs/main.c $(CC) $(CFLAGS) -o $@ $< fs/open.o: fs/open.c $(CC) $(CFLAGS) -o $@ $< fs/read_write.o: fs/read_write.c $(CC) $(CFLAGS) -o $@ $< fs/link.o: fs/link.c $(CC) $(CFLAGS) -o $@ $< fs/disklog.o: fs/disklog.c $(CC) $(CFLAGS) -o $@ $<
![]() |
| 執行畫面(最後成果) |
![]() |
| 執行畫面(直接從硬碟啟動,尚未從硬碟啟動) |
![]() |
| 執行畫面(必要動作,從軟碟安裝作業系統至硬碟) |
注意事項
第一次必須從軟碟開機:因為硬碟裡面沒有 loader 和 kernel(類似於安裝作業系統至硬碟)。軟碟作兩件事:
- 透過 mkfs() 將硬碟的相應分區做成 Orange'S FS
- 將 cmd.tar 解開,這時 FS 中就有 hdldr.bin 和 kernel.bin
目錄結構
. ├── 80m.img ├── 80m.vdi ├── a.img ├── boot │ ├── boot.asm │ ├── hdboot.asm │ ├── hdldr.asm │ ├── include │ │ ├── fat12hdr.inc │ │ ├── load.inc │ │ └── pm.inc │ └── loader.asm ├── command │ ├── echo.c │ ├── hdldr.bin │ ├── inst.tar │ ├── kernel.bin │ ├── Makefile │ ├── pwd.c │ └── start.asm ├── fs │ ├── disklog.c │ ├── link.c │ ├── main.c │ ├── misc.c │ ├── open.c │ └── read_write.c ├── include │ ├── stdio.h │ ├── string.h │ ├── sys │ │ ├── config.h │ │ ├── console.h │ │ ├── const.h │ │ ├── fs.h │ │ ├── global.h │ │ ├── hd.h │ │ ├── keyboard.h │ │ ├── keymap.h │ │ ├── proc.h │ │ ├── protect.h │ │ ├── proto.h │ │ ├── sconst.inc │ │ └── tty.h │ └── type.h ├── kernel │ ├── clock.c │ ├── console.c │ ├── global.c │ ├── hd.c │ ├── i8259.c │ ├── kernel.asm │ ├── keyboard.c │ ├── kliba.asm │ ├── klib.c │ ├── main.c │ ├── proc.c │ ├── protect.c │ ├── start.c │ ├── systask.c │ └── tty.c ├── lib │ ├── close.c │ ├── exec.c │ ├── exit.c │ ├── fork.c │ ├── getpid.c │ ├── lseek.c │ ├── misc.c │ ├── open.c │ ├── printf.c │ ├── read.c │ ├── stat.c │ ├── string.asm │ ├── syscall.asm │ ├── syslog.c │ ├── unlink.c │ ├── vsprintf.c │ ├── wait.c │ └── write.c ├── Makefile └── mm ├── exec.c ├── forkexit.c └── main.c



留言
張貼留言