FreeFlyOS【一】:boot部分(引導扇區)詳解

cy295957410發表於2021-01-03

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。

相關文章