FreeFlyOS【一】:boot部分(引導扇區)詳解
boot.ld
/*
** 連結指令碼
*/
OUTPUT_FORMAT(elf32-i386)
OUTPUT_ARCH(i386)
ENTRY(start)
/*
* ld有多種方法設定程式入口地址, 按以下順序: (編號越前, 優先順序越高)
* 1, ld命令列的-e選項
* 2, 連線指令碼的ENTRY(SYMBOL)命令
* 3, 如果定義了start 符號, 使用start符號值
* 4, 如果存在 .text section , 使用.text section的第一位元組的位置值
* 5, 使用值0
*
*
*/
SECTIONS
{
/* 將定位器符號置為0x7c00 */
. = 0x7C00;
/*
將所有(*符號代表任意輸入檔案)輸入檔案bootsector.S的.start section合併
成一個.start section, 該section的地址由定位器符號的值
指定, 即0x7c00.
bootsector.o整體作為一個start節
*/
.start : {
*bootsector.o(.text)
}
/*
將所有(*符號代表任意輸入檔案)輸入檔案的.text section合併
成一個.text section, 該section的地址緊接.start節.
bootmain.o中的text作為一個text節
*/
.text : { *(.text) }
/*
將所有(*符號代表任意輸入檔案)輸入檔案的.data section合併
成一個.data section, 該section的地址緊接.text節.
bootmain.o中的data作為一個data節
*/
.data : { *(.data .rodata) }
/DISCARD/ : { *(.eh_*) }
}
bootsector.S
/*
* 主要功能:關中斷、記憶體探測、80x86 CPU從真實模式變成保護模式
* 跳轉到載入核心的32位程式碼段
* 注意:本檔案不是MBR(512B),而是和bootmain.c連結成MBR
*/
.text
.code16 #.code16表示16位程式碼段
.global start
/*
*將ds、es和ss段暫存器均設定成cs段暫存器的值,並將棧頂指標esp指向
*0x7c00,棧向低地址增長。這步操作其實也可省略,因為在16位程式碼段中
*還用不到其他段暫存器,在需要使用的時候再初始化不遲
*/
start:
movw %cs,%ax
movw %ax,%ds # ->Data Segment
movw %ax,%es # ->Extra Segment
movw %ax,%ss # ->Stack Segment
movl $0x7C00,%esp
/*
*關中斷,在後面我們在記憶體中會建立中斷向量表,所以事先關好中斷,
*防止在建表過程中來了中斷,所以事先遮蔽,防止這種情況產生。
*/
cli
/* 記憶體探測,記憶體地址0x8000作為記憶體探測段數的儲存地址,
方便後面呼叫 */
movw $0,0x8000
movw $0x8004,%di
xor %ebx,%ebx
mm_probe:
movl $0xe820,%eax
movl $20,%ecx
movl $0x534D4150,%edx
int $0x15
#產生進位則跳轉
jnc cont
jmp probe_end
cont:
incl 0x8000
addw $20,%di
cmpl $0,%ebx
jnz mm_probe
probe_end:
/*
*開啟地址線A20。實際上若我們使用qemu跑這個程式時,A20預設開啟,
*但為了相容性,最好還是手動將A20地址線開啟.讀者可以試一試將開啟
*A20程式碼刪去後,在保護模式(32位程式碼段#)下用回滾機制測試時是否
*仍然顯示字元
*
*8042(鍵盤控制器)埠的P21和A20相連,置1則開啟
*0x64埠 讀:位1=1 輸入緩衝器滿(0x60/0x64口有給8042的資料)
*0x64埠 寫: 0xd1->寫8042的埠P2,比如位2控制P21
*當寫時,0x60埠提供資料,比如0xdf,這裡面把P2置1
*
*由於MacOS下編譯器的版本原因,若加上下面程式碼會超出512B,故舍去
*/
/*waitforbuffernull1:
#先確定8042是不是為空,如果不為空,則一直等待
xorl %eax,%eax
inb $0x64,%al
testb $0x2,%al
jnz waitforbuffernull1
#8042中沒有命令,則開始向0x64埠發出寫P2埠的命令
movb $0xd1,%al
outb %al,$0x64
waitforbuffernull2:
#再確定8042是不是為空,如果不為空,則一直等待
xorl %eax,%eax
inb $0x64,%al
testb $0x2,%al
jnz waitforbuffernull2
#向0x60埠傳送資料,即把P2埠設定為0xdf
movb $0xdf,%al
outb %al,$0x60*/
/* 載入gdt表,即將記憶體中的gdt基址和表長寫入GDTR暫存器 */
lgdt gdt_48
/* 開啟保護模式,將cr0的位0置為1,一般而言BIOS中斷
只在真實模式下進行呼叫 */
movl %cr0,%eax
orl $0x1,%eax
movl %eax,%cr0
/*
*進入到32位程式碼段。0x8代表段選擇子(16位)——0000000000001000
*其中最後2為代表特權級.linux核心只使用了2個特權級(0和3),00代表
*0特權級(核心級),倒數第3位的代表是gdt(全域性描述符表)還是
*idt(區域性描述符表),0代表全域性描述符表,前13位代表gdt的項數(第1項),
*屬於程式碼段。所以0x8代表特權級為0(核心級)的全域性程式碼段,promode代表
*偏移地址。
*/
ljmp $0x8,$promode
/* 保護模式下的32位程式碼段 */
promode:
.code32
movw $0x10,%ax
movw %ax,%ds #->Data Segment
movw %ax,%es #->Extra Segment
movw %ax,%ss #->Stack Segment
movw $0x18,%ax
movw %ax,%gs
movl $0x0,%ebp
movl $start,%esp
/* 呼叫bootmain.c中的bootmain函式 */
call bootmain
/* 在記憶體中做一塊GDT表 */
.align 2
gdt:
.word 0,0,0,0
.word 0xFFFF #第1項CS,基地址為0,限長
.word 0x0000
.word 0x9A00
.word 0x00CF
.word 0xFFFF #第2項DS,基地址為0
.word 0x0000
.word 0x9200
.word 0x00CF
.word 0xFFFF #第3項VGA,基地址位0xb8000
.word 0x8000
.word 0x920b
.word 0x0000
/*
*將gdtr專用暫存器指向我們在記憶體中做的一塊GDT表,GDTR暫存器格式:
*48位(高32位地址+低16位限長),intel是小端方式
*/
gdt_48:
.word 0x1f #gdt表限長 sizeof(gdt)-1 低地址,放在gdtr的低位元組
.long gdt #gdt表基址 高地址,放在gdtr的高位元組
bootmain.c
/*
* 主要功能:將elf格式的核心程式碼讀入到指定記憶體區域中
* 本檔案和bootsector.S組成MBR
* 注意: MBR檔案末尾以0xaa55結束,需sign檔案
* 格式化成合法的MBR
*/
#define SECTSIZE 512
#define ELFHDR ((struct elfhdr *)0x10000) // scratch space
#define ELF_MAGIC 0x464C457FU //0x464C457FU
/* elf檔案頭 */
struct elfhdr{
unsigned int magic; // must equal ELF_MAGIC
unsigned char elf[12];
unsigned short type; // 1=relocatable, 2=executable, 3=shared object, 4=core image
unsigned short machine; // 3=x86, 4=68K, etc.
unsigned int version; // file version, always 1
unsigned int entry; // entry point if executable
unsigned int phoff; // file position of program header or 0
unsigned int shoff; // file position of section header or 0
unsigned int flags; // architecture-specific flags, usually 0
unsigned short ehsize; // size of this elf header
unsigned short phentsize;// size of an entry in program header
unsigned short phnum; // number of entries in program header or 0
unsigned short shentsize;// size of an entry in section header
unsigned short shnum; // number of entries in section header or 0
unsigned short shstrndx; // section number that contains section name strings
};
/* 程式頭 */
struct proghdr {
unsigned int p_type; // loadable code or data, dynamic linking info,etc.
unsigned int p_offset; // file offset of segment
unsigned int p_va; // virtual address to map segment
unsigned int p_pa; // physical address, not used
unsigned int p_filesz; // size of segment in file
unsigned int p_memsz; // size of segment in memory (bigger if contains bss)
unsigned int p_flags; // read/write/execute bits
unsigned int p_align; // required alignment, invariably hardware page size
};
/*
* inb(port):從port埠中讀取一個位元組資料返回
*/
static inline unsigned char inb(unsigned short port) {
unsigned char data;
asm volatile ("inb %1, %0" : "=a" (data) : "d" (port));
return data;
}
/*
*insl(port,addr,cnt):從port埠迴圈讀cnt次雙字到addr位置
*
*cld指令是使DF=0, 即si,di暫存器自動增加
*
*rep指令的目的是重複其上面的指令.ECX的值是重複的次數.repe和repne,
*前者是repeat equal,意思是相等的時候重複,後者是repeat not equal,
*不等的時候重複;每迴圈一次cx自動減一。
*
*insl 指令是從 DX 指定的 I/O 埠將雙字輸入 ES:(E)DI 指定的記憶體位置
*
*/
static inline void insl(unsigned int port, void *addr, int cnt) {
asm volatile (
"cld;"
"repne; insl;"
: "=D" (addr), "=c" (cnt)
: "d" (port), "0" (addr), "1" (cnt)
: "memory", "cc");
}
/*
* outb(port,data):將一個位元組資料data讀入port埠中
*/
static inline void outb(unsigned short port, unsigned char data) {
asm volatile ("outb %0, %1" :: "a" (data), "d" (port));
}
/*
* waitdisk:等待硬碟準備好
*/
static inline void waitdisk(void) {
while ((inb(0x1F7) & 0xC0) != 0x40)
;
}
/*
* readsect(dst,secno):讀取扇區號secno所在的扇區進入dst地址中
*/
static inline void readsect(void *dst, unsigned int secno) {
// 等待硬碟準備好
waitdisk();
outb(0x1F2, 1); // count = 1
outb(0x1F3, secno & 0xFF);
outb(0x1F4, (secno >> 8) & 0xFF);
outb(0x1F5, (secno >> 16) & 0xFF);
outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
outb(0x1F7, 0x20); // 命令0x20 - 讀取扇區
// 等待硬碟準備好
waitdisk();
// 讀取一個扇區
insl(0x1F0, dst, SECTSIZE / 4);
}
/*
* readseg(va,count,offset):讀取核心基址偏移為offset處的count位元組
* 放入虛擬地址va中。
*/
static void readseg(unsigned int va, unsigned int count, unsigned int offset) {
unsigned int end_va = va + count;
// round down to sector boundary
va -= offset % SECTSIZE;
// translate from bytes to sectors; kernel starts at sector 1
unsigned int secno = (offset / SECTSIZE) + 1;
// If this is too slow, we could read lots of sectors at a time.
// We'd write more to memory than asked, but it doesn't matter --
// we load in increasing order.
for (; va < end_va; va += SECTSIZE, secno ++) {
readsect((void *)va, secno);
}
}
/*
* bootmain():讀取第1號扇區中的核心的ELF頭,獲取程式段頭資訊
* 並把所有程式段讀入記憶體的相應虛擬地址中
* 由於此時未開分頁機制,虛擬地址=實體地址
* 最後進入ELF頭的入口地址,即核心地址
*/
void bootmain(void) {
// read the 1st page off disk
readseg((unsigned int )ELFHDR, SECTSIZE * 2, 0);
struct proghdr *ph, *eph;
// load each program segment (ignores ph flags)
ph = (struct proghdr *)((unsigned int )ELFHDR + ELFHDR->phoff);
eph = ph + ELFHDR->phnum;
unsigned int mask;
//由於核心放在16MB處,至少需要28位對齊(0xFFFFFFF)
for (; ph < eph; ph ++) {
//qemu特性決定
if(ph->p_va > (unsigned int )0xC0000000){
mask=0xFFFFFFF;
}
else{
mask=0xFFFFFFFF;
}
readseg(ph->p_va & mask, ph->p_memsz, ph->p_offset);
}
// call the entry point from the ELF header
// note: does not return
((void (*)(void))(ELFHDR->entry & 0xFFFFFFFF))();
while (1);
}
sign.c
/*
* 主要功能:在bootsector.S和bootmain.c連結成的
* bootblock.out檔案末尾新增0xaa55結束符
* 注意: 合法的MBR檔案末尾以0xaa55結束。
*
*/
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
/*
* main(argc,argv):在第一個引數argv[1]檔案的末尾
* 新增0x55AA,然後寫入第二個引數argv[2]檔案中
*/
int main(int argc, char *argv[]) {
struct stat st;
if (argc != 3) {
fprintf(stderr, "Usage: <input filename> <output filename>\n");
return -1;
}
if (stat(argv[1], &st) != 0) {
fprintf(stderr, "Error opening file '%s': %s\n", argv[1], strerror(errno));
return -1;
}
printf("'%s' size: %lld bytes\n", argv[1], (long long)st.st_size);
if (st.st_size > 510) {
fprintf(stderr, "%lld >> 510!!\n", (long long)st.st_size);
return -1;
}
char buf[512];
memset(buf, 0, sizeof(buf));
FILE *ifp = fopen(argv[1], "rb");
int size = fread(buf, 1, st.st_size, ifp);
if (size != st.st_size) {
fprintf(stderr, "read '%s' error, size is %d.\n", argv[1], size);
return -1;
}
fclose(ifp);
buf[510] = 0x55;
buf[511] = 0xAA;
FILE *ofp = fopen(argv[2], "wb+");
size = fwrite(buf, 1, 512, ofp);
if (size != 512) {
fprintf(stderr, "write '%s' error, size is %d.\n", argv[2], size);
return -1;
}
fclose(ofp);
printf("build 512 bytes boot sector: '%s' success!\n", argv[2]);
return 0;
}
CMakeLists.txt
#設定專案名
project (bootblock C ASM)
add_library(bootsector OBJECT bootsector.S)
add_library(bootmain OBJECT bootmain.c)
#連結
add_executable(${PROJECT_NAME}.o
$<TARGET_OBJECTS:bootsector>
$<TARGET_OBJECTS:bootmain>
)
target_link_options(${PROJECT_NAME}.o PRIVATE -T ${FreeFlyOS_SOURCE_DIR}/boot/boot.ld)
target_link_options(${PROJECT_NAME}.o PRIVATE -Wl,-melf_i386)
add_custom_command(TARGET ${PROJECT_NAME}.o
POST_BUILD
COMMAND
${CMAKE_OBJCOPY} -S -O binary ${PROJECT_NAME}.o ${PROJECT_NAME}.out
COMMAND
gcc ${FreeFlyOS_SOURCE_DIR}/boot/sign.c -o sign
COMMAND
./sign ${PROJECT_NAME}.out ${PROJECT_NAME}
)
1、編譯bootsector.S以及bootmain.c檔案生成bootsector.o和bootmain.o
2、將boosector.o和bootmain.o連結到0x7c00地址處生成bootblock.o
3、使用objcopy將bootblack.o複製生成可執行檔案bootblock.out
4、使用格式化工具sign將bootblack.out格式化成MBR
5、之後為了給主分割槽表預留空間,該MBR從0x1BE-0x1FD的空間為主分割槽表資訊,本專案的MBR如下圖所示。
功能說明
作為一個核心的bootloader,它的主要功能如下:
1、在16位程式碼段下,應先設定段暫存器(DS、ES、SS)的值(CS預設上電賦值)。
2、關中斷,為之後建立中斷向量表打好基礎。
3、記憶體探測,通過BIOS提供的0x15中斷獲取可用記憶體資料,並儲存在記憶體實體地址0x8000處。
4、開啟地址A20,按理說這步應該是要做的,但由於本人環境編譯器版本問題,加上這段程式碼會使MBR超過512B,故只能刪掉這部分程式碼,經過測試,在qemu模擬器不影響後續程式碼的執行,以此來減少MBR的空間,併為主分割槽表預留空間。
5、載入gdt表,其中設定了CS和DS段基址為0,段限長為4GB,這樣就能訪問0-4GB的實體地址了,而實際分配給虛擬機器2GB記憶體,為後面地址對映作準備。
6、接著就是開啟保護模式了,把cr0的第0位置1即可,此時就無法呼叫BIOS中斷了,所以如果需要用BIOS中斷獲取硬體資訊,最好在保護模式前就寫好。
7、使用ljmp長跳轉到保護模式下的32位程式碼段,該段的屬性已經在gdt表中寫好,只需確定跳轉程式碼的偏移地址即可。
8、上一步操作已經確定了CS段,而資料段並未確定,所以需要設定DS、ES、SS段的值,這裡還設定了gs段的值,之前主要是為了能夠用VGA輸出字元,但後續直接寫了VGA的驅動,所以這裡的GS段設定和之前GDT中的VGA段均可刪掉。
9、設定下棧地址,棧頂指標指向0x7c00,也就是bootloader開始的位置,但棧是向下增長的,故不會影響bootloader程式碼,在bootmain.c中會呼叫函式,故需要一個棧來存放引數和返回地址等資訊。
10、將核心載入到記憶體中,一般核心檔案都是elf檔案格式,為了能夠讀取核心的elf頭,我們將核心檔案放在引導扇區MBR後面一個扇區,通過x86_64-elf-readelf -a build/kernel/kernel可以看到核心的elf檔案佈局,如下圖所示。
從這張圖中我們能看到整個核心檔案基本分為3個部分:init部分(.init.text/data)、user部分(.user.text/data/rodata/bss)、kernel部分(.text/rodata/data/bss),各個部分的段地址由連結指令碼決定,由於這個時候我們還沒開啟分頁,虛擬地址=實體地址,而且實際實體記憶體只有2G,所以核心部分(即從C1000000開始)的程式碼,故在bootmain中需要進行一個判斷,若虛擬地址大於2G,即核心程式碼,放在0x1000000處,當然,也可以設定qemu模擬器的記憶體大小,比如分配4G,但要注意的一點是,當分配給qemu模擬器4G記憶體中,可用只有3.5G,此後到4G之間的實體地址無法使用。
然後就是通過IO埠和硬碟進行互動,將各個部分的各個段讀到對應的虛擬地址上(虛擬地址=實體地址),最後跳轉到ELF檔案的entry中,即0x100000(init部分的程式碼)。
12、boot下的連結指令碼,如下圖所示,0x7C00為約定好的系統引導地址,即BIOS執行完後,會自動執行0x7C00開始的bootloader。
13、關於CMakeLists.txt的一些解釋
1⃣️project ( )------首先設定專案名稱,一般這個可以隨意設定,然後要註明使用了哪些語言,比如C語言,ASM彙編,若不指定則在編譯時會自動看成C語言,.S檔案就編譯不了,需要在上層目錄下的CMakeLists.txt下使能編譯器。
2⃣️add_library------將原始檔編譯成靜態庫檔案,可以把它看成x86_64-elf-gcc(MAC下)/gcc(Ubuntu下) -c bootsector.c -o bootsector.o,當然這個編譯選項在上一層也就是整個專案目錄下的CMakeLists.txt確定的,如下圖所示。
具體說明下每個引數是啥意思吧:
-Os:主要對程式的尺寸進行優化,為了減少MBR的大小,可謂絞盡腦汁。
-nostdlib:不連線系統標準啟動檔案和標準哭檔案,只把指定的檔案傳遞給連線庫,這樣我們就能重寫printf等,不會和標準C庫重名了。
-fno-builtin:不使用C語言的內建函式,所以我們設定的函式名可以和內建函式同名。
-Wall:顯示所有警告。
-ggdb:產生debug資訊,用於gdb除錯
-m32:生成32位機器的彙編程式碼
-gstabs:以stabs格式聲稱除錯資訊,但是不包括gdb除錯資訊。
-nostdinc:不包含C語言的標準庫的標頭檔案。
-fno-stack-protector:不使用棧保護檢測。
3⃣️add_executable------將靜態庫檔案連結成可執行檔案,實際使用時為x86_64-elf-ld命令(MAC下)/ld命令(Ubuntu下)
4⃣️target_link_options-----設定連結選項,-T指定連結指令碼,-Wl 傳遞引數 ,-melf_i386連結為32位程式
5⃣️add_custom_command-----在生成專案檔案後,繼續執行指定的命令,比如把.o檔案轉化為二進位制檔案,然後通過格式化檔案sign將bootloader格式化為MBR。
最後,boot目錄大概就總結這麼多吧,有不懂的地方隨時聯絡我,295957410@qq.com,專案原始碼地址為https://github.com/dashanji/FreeFlyOS。
相關文章
- 主引導扇區的理解
- 作業系統1——引導扇區的理解作業系統
- 寫作業系統之開發引導扇區作業系統
- 硬碟主引導扇區的結構及功能全面釋疑(轉)硬碟
- 從扇區看USB-FDD和USB-HDD在引導時的區別
- 製作隨身碟系統盤時提示引導扇區未知的原因與解決方法
- histb 引導核心 boot_cmd 引數含義boot
- Mysql配置引數詳解(一)MySql
- XYCTF pwn部分題解 (部分題目詳解)
- checkpoint詳解(部分轉)
- 一文詳解Spring Boot的使用Spring Boot
- CSS3扇形旋轉效果詳解CSSS3
- Linux 開機引導和啟動過程詳解Linux
- Spring Boot Security 詳解Spring Boot
- 專題一之Spring Boot入門詳解Spring Boot
- Java之Spring Boot詳解JavaSpring Boot
- Spring Boot(八):RabbitMQ 詳解Spring BootMQ
- 全面瞭解遊戲引導:6大引導形式,哪個最好?遊戲
- ajax 引數詳解
- DockerFile引數詳解Docker
- dd引數詳解
- Mysqldump引數詳解MySql
- vmstat 引數詳解
- st foc 扇區判斷解析
- macOS Ventura 13.0 (22A380) Boot ISO 原版可引導映象Macboot
- Spring Boot Admin 2.0 詳解Spring Boot
- Spring Boot(四):Thymeleaf 使用詳解Spring Boot
- Spring Boot 中使用 Dubbo 詳解Spring Boot
- OGG引數詳解
- tar命令引數詳解
- 函式引數詳解函式
- Oracle UNDO引數詳解Oracle
- jqGrid引數詳解
- $.ajax()方法引數詳解
- DataGuard引數配置詳解
- redis info引數詳解Redis
- ES常用引數詳解
- struts配置引數詳解