bootmain.c

/* bootmain - the entry of bootloader */
void
bootmain(void) {
    // read the 1st page off disk
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

    // is this a valid ELF?
    if (ELFHDR->e_magic != ELF_MAGIC) {
        goto bad;
    }

    struct proghdr *ph, *eph;

    // load each program segment (ignores ph flags)
    ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
    eph = ph + ELFHDR->e_phnum;
    for (; ph < eph; ph ++) {
        readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
    }

    // call the entry point from the ELF header
    // note: does not return
    ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

bad:
    outw(0x8A00, 0x8A00);
    outw(0x8A00, 0x8E00);

    /* do nothing */
    while (1);
}

bootmain.c中的bootmain函数如上,接下来我们来逐步分析一下这个函数。但是里面更多的和硬件相关的东西,我也不是特别清楚,所以在深入层次上会做一个平衡。

// read the 1st page off disk
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

而在bootmain.c的文件开头定义里这两个常量:

#define SECTSIZE        512
#define ELFHDR          ((struct elfhdr *)0x10000)      // scratch space

意思就是从磁盘读8个扇区的大小到ELFHDR这个地址上,偏移量为0,接下来看看readseg函数。

/* *
 * readseg - read @count bytes at @offset from kernel into virtual address @va,
 * might copy more than asked.
 * */
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
    uintptr_t end_va = va + count;

    // round down to sector boundary
    va -= offset % SECTSIZE;

    // translate from bytes to sectors; kernel starts at sector 1
    uint32_t 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);
    }
}

函数会从内核的offset位置(因为内核起始位置为一个扇区的起始位置,所以offset % 512 也就是 offset这个位置相对于这个扇区的offset)读取count个字节,放置到va所对应的虚拟内存位置。函数传进来的offset和(offset % sectsize)意思不一样,前者是相对于1号扇区开始位置的偏移量,后者是相对于你要读的那个扇区的偏移量。
va -= offset % SECTSIZE; 这条语句表明我们读字节必须从扇区头部开始读;所以将va减去offset,最后的效果是会多读一点进来(va前面的),但是没有关系。 secno则是调用readsect函数必需的扇区号(从1开始,可以认为0号扇区存放BIOS和bootloader,不知道这么说对不对)

接下来看readsect函数

/* readsect - read a single sector at @secno into @dst */
static void
readsect(void *dst, uint32_t secno) {
    // wait for disk to be ready
    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);                      // cmd 0x20 - read sectors

    // wait for disk to be ready
    waitdisk();

    // read a sector
    insl(0x1F0, dst, SECTSIZE / 4);
}

先看waitdisk

/* waitdisk - wait for disk ready */
static void
waitdisk(void) {
    while ((inb(0x1F7) & 0xC0) != 0x40)
        /* do nothing */;
}

这个函数用了GNU 扩展内联汇编

static inline uint8_t
inb(uint16_t port) {
    uint8_t data;
    asm volatile ("inb %1, %0" : "=a" (data) : "d" (port));
    return data;
}

意思是从指定端口读取一个字节的数据并返回

inb和outb不多说我们看看insl

static inline void
insl(uint32_t port, void *addr, int cnt) {
    asm volatile (
            "cld;"
            "repne; insl;"
            : "=D" (addr), "=c" (cnt)
            : "d" (port), "0" (addr), "1" (cnt)
            : "memory", "cc");
}

首先是下面的约束条件,指定了addr 放到%edi, cnt 放到%ecx,port放到%edx,注意addr,cnt都是既是输入型参数也是输出型参数。 cld指令是和repne指令配合使用的,repne指令是连续执行下一条指令,直到%ecx为0.cld指令初始化%ecx减小的步长为1.
第三个约束clobber说明,在指令执行过程中,内存和cc(这到底是啥?也许是条件码吧)可能会被改变。

综合起来就是每次读入4个字节的数据到addr,读入cnt次。所以read a sector这行注释的下面次数是扇区的字节数除以4

而中间一串outb的就是在向指定端口写读磁盘的指令,这个属于太细节的硬件问题,略过。

当我们完成了读入kernel的ELF文件头之后,就要来解析这个ELF文件头。

// is this a valid ELF?
    if (ELFHDR->e_magic != ELF_MAGIC) {
        goto bad;
    }

这里就要提一嘴ELFHDR

#ifndef __LIBS_ELF_H__
#define __LIBS_ELF_H__

#include <defs.h>

#define ELF_MAGIC    0x464C457FU            // "\x7FELF" in little endian 小端机

/* file header */
struct elfhdr {
    uint32_t e_magic;     // must equal ELF_MAGIC
    uint8_t e_elf[12];
    uint16_t e_type;      // 1=relocatable, 2=executable, 3=shared object, 4=core image
    uint16_t e_machine;   // 3=x86, 4=68K, etc.
    uint32_t e_version;   // file version, always 1
    uint32_t e_entry;     // entry point if executable
    uint32_t e_phoff;     // file position of program header or 0 可执行文件头的结构体数组基地址
    uint32_t e_shoff;     // file position of section header or 0
    uint32_t e_flags;     // architecture-specific flags, usually 0
    uint16_t e_ehsize;    // size of this elf header
    uint16_t e_phentsize; // size of an entry in program header
    uint16_t e_phnum;     // number of entries in program header or 0 可执行文件头的个数
    uint16_t e_shentsize; // size of an entry in section header
    uint16_t e_shnum;     // number of entries in section header or 0
    uint16_t e_shstrndx;  // section number that contains section name strings
};

/* program section header */
struct proghdr {
    uint32_t p_type;   // loadable code or data, dynamic linking info,etc.
    uint32_t p_offset; // file offset of segment  代码段相对于本结构的偏移量
    uint32_t p_va;     // virtual address to map segment
    uint32_t p_pa;     // physical address, not used
    uint32_t p_filesz; // size of segment in file
    uint32_t p_memsz;  // size of segment in memory (bigger if contains bss)内存映像中代码段的大小
    uint32_t p_flags;  // read/write/execute bits
    uint32_t p_align;  // required alignment, invariably hardware page size
};

#endif /* !__LIBS_ELF_H__ */

如果ELF文件头的指定位置不是这个ELF_MAGIC的话那么我们不认为这是一个有效的文件头

	struct proghdr *ph, *eph;

    // load each program segment (ignores ph flags)
    ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
    eph = ph + ELFHDR->e_phnum;
    for (; ph < eph; ph ++) {
        readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
    }

接下来的代码让我们主要关心上面结构体的定义中,我标了中文注释的部分。

主要意思就是将整个ELF文件中存放在不同位置的代码段都读入内存,需要的信息就是第一个struct program header*的位置,和这个数组的长度。然后根据progeam header将可执行代码读入到内存。

// call the entry point from the ELF header
    // note: does not return
    ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

最后调用了kernel的程序入口点,这个写得有点绕,我们拆开来看

();是函数调用,那么前面得东西就必然是个函数指针,我们知道目前ELFHDR->e_entry & 0xFFFFFF还只是个数字,那么我们把它强制类型转换, (void (*)(void))就是对一个函数指针类型的描述,这个指针描述的函数时返回值为void,参数列表也为void的函数,星号后面没跟名字就是匿名的函数指针变量。最后调用它,启动操作系统内核。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值