如何使用STM32F4的BootLoader和APP程序

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何使用STM32F4的BootLoader和APP程序相关的知识,希望对你有一定的参考价值。

参考技术A 使用ISP下载时必须使用串口1,启动模式必须设成system Flash模式,下载完再改成main Flash模式,ISP软件可使用ST官方提供的

ucore lab1 练习4—bootloader加载ELF格式OS

练习4:分析bootloader加载ELF格式的OS的过程。(要求在报告中写出分析)

通过阅读bootmain.c,了解bootloader如何加载ELF文件。通过分析源代码和通过qemu来运行并调试bootloader&OS。

  • bootloader如何读取硬盘扇区的?
  • bootloader是如何加载ELF格式的OS?

bootloader如何读取硬盘扇区的?

内联汇编函数

bootloader以及OS在执行时需要直接使用一些汇编指令,如常用的IO指令,采用内联汇编的方式将常用的汇编操作封装为函数,有利于复用,头文件x86.h为一些常用的内联汇编函数封装。

如下面两个函数:

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

inb函数作用为从port IO端口读取一字节数据作为该函数的返回值,data变量为输出,使用al寄存器,占位符为%0,port变量为输入,使用dx寄存器,占位符为%1。

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");
}

insl函数作用为从port IO端口读取cnt个双字(4Byte)大小的数据到基址为addr的内存中。

输出变量:addr使用edi,cnt使用ecx;

输入变量:port使用edx,addr使用edi(0代表输出输入变量的第0个),cnt使用ecx;

约束:执行过程中内存会发生改变,状态寄存器会改变。

cld为清除方向状态寄存器,即设置df = 0,edi移动方向为内存递增,

repne代表重复下一字符串操作指令若干次,直到ecx = 0或ZF= 1,也即重复insl指令cnt次(ecx值),

insl为从端口edx中输入双字到es:edi所指的内存中,也即addr处。

磁盘访问

磁盘的读取方式主要有两种CHS(Cylinder Head Sector)、LBA(Logical Block Address),CHS通过柱面-磁头-扇区三个值进行定位,较为麻烦;LBA方式进一步屏蔽了细节,将磁盘按照扇区号进行统一编址,访问时给出扇区号即可。LBA有LBA28和LBA48两种,分别表示扇区地址为28位和48位。

第一个IDE通道通过IO地址0x1f0-0x1f7访问,通过0x1f6端口可以设置主从盘、访问方式以及扇区号27—24位,如下图所示:

技术图片

磁盘IO地址与相应功能如下表:

IO地址 功能
0x1f0 读数据,当0x1f7不为忙状态时,可以读。
0x1f2 要读写的扇区数,每次读写前,你需要表明你要读写几个扇区。最小是1个扇区
0x1f3 如果是LBA模式,就是LBA参数的0-7位
0x1f4 如果是LBA模式,就是LBA参数的8-15位
0x1f5 如果是LBA模式,就是LBA参数的16-23位
0x1f6 第0~3位:如果是LBA模式就是24-27位 第4位:为0主盘;为1从盘
0x1f7 状态和命令寄存器。操作时先给命令,再读取,如果不是忙状态就从0x1f0端口读数据
#define SECTSIZE        512

/* waitdisk - 等待磁盘准备就绪 */
static void
waitdisk(void) {
    while ((inb(0x1F7) & 0xC0) != 0x40)
        /* 死循环等待 */;
}

读取0x1f7端口来获取磁盘控制器状态,若第7位为1,表示磁盘正忙,需要等待,若第7位为0,第6位为1,表示准备就绪。

技术图片

/* readsect - read a single sector at @secno into @dst */
/* 读取编号为secno的一个扇区到dst地址处 */
static void
readsect(void *dst, uint32_t 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);  //insl每次读取4B,cnt为 扇区长度 / 4
}
/* *
 * readseg - read @count bytes at @offset from kernel into virtual address @va,
 * might copy more than asked.
 * 从内核的offset偏移处读取count个字节到虚拟地址va中,可能复制的字节数多于请求的count个
 * */
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
    uintptr_t end_va = va + count;  //读取地址尾部

    // 向下对准到磁盘边界
    va -= offset % SECTSIZE;

    // 将偏移量转为扇区号,内核始于扇区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);  //依次读取若干扇区
    }
}

因为readsect函数每次只完整读取一个扇区,考虑到readseg的偏移读取,通过va -= offset % SECTSIZE来向下对准到磁盘边界,因此如果读取磁盘中内核文件的起始数据不位于一个扇区的开始,应将va减小,使得完整读取一个扇区后,原va处正好就是内核文件经过偏移offset后的起始数据。

bootloader是如何加载ELF格式的OS?

ELF(Executable and linking format)文件格式是Linux系统下一种常用的目标文件(object file)格式,主要有可执行文件、重定向文件、共享目标文件三种类型。

ELF链接文件和可执行文件基本布局如下图所示:

技术图片

此实验编译出的内核文件为ELF格式的可执行文件,主要有ELF文件头、程序头表、以及相应段组成。

ELF文件头用于描述该ELF文件的元信息,其结构体如下所示:

#define ELF_MAGIC    0x464C457FU            // "x7FELF" in little endian  ELF魔数,小端存储

/* ELF文件头 */
struct elfhdr {
    uint32_t e_magic;     // ELF魔数,7f 45 4c 46
    uint8_t e_elf[12];    // ELF格式信息,32/64位,大端/小端存储
    uint16_t e_type;      // 1=relocatable, 2=executable, 3=shared object, 4=core image
    uint16_t e_machine;   // ELF体系结构类型,3=x86, 4=68K, etc.
    uint32_t e_version;   // 文件版本,总为1
    uint32_t e_entry;     // 可执行程序入口虚拟地址
    uint32_t e_phoff;     // 程序头表在文件内的字节偏移量
    uint32_t e_shoff;     // 节头表在文件内的字节偏移量
    uint32_t e_flags;     // 处理器相关标识,通常为0
    uint16_t e_ehsize;    // ELF header文件头字节大小
    uint16_t e_phentsize; // 程序头表每个条目(entry)的字节大小
    uint16_t e_phnum;     // 程序头表条目数量,即段的个数
    uint16_t e_shentsize; // 节头表每个条目(entry)的字节大小
    uint16_t e_shnum;     // 节头表中条目的数量,即节的个数
    uint16_t e_shstrndx;  // string name table在节头表中的索引index
};

程序头表用来描述程序中的各段信息,如代码段、数据段等,这些段组成了最终在内存中执行的程序。程序头表提供了各段在虚拟地址空间和物理地址空间中的大小、位置、标志、访问授权和对齐方面的信息。程序头表结构如下所示:

struct proghdr {
    uint32_t p_type;   // 段类型, 如可加载的代码段或数据段、动态链接信息段等
    uint32_t p_offset; // 本段在文件内的其实偏移
    uint32_t p_va;     // 本段在内存中的起始虚拟地址
    uint32_t p_pa;     // 本段在内存中的起始物理地址,不使用
    uint32_t p_filesz; // 本段在文件中的大小
    uint32_t p_memsz;  // 本段在内存中的大小(如果包含bss节,会更大)
    uint32_t p_flags;  // 本段相关的标志,如读/写/执行
    uint32_t p_align;  // 对齐,和硬件页大小一致;如果为0或1,表示不需要对齐。否则,p_align应该是2的正整数幂,p_va和p_offset在对p_align取模后应相等
};

通过readelf -e kernel可获取kernel ELF文件的相关信息,ELF头信息如下所示:

技术图片

可见程序入口虚拟地址为0x100000,程序头表偏移为52B,程序头表条目为3。

程序头表信息如下所示:

技术图片

共有3个段,实际为前两个,最后一个是空段,第一个段是代码段,第二个段是数据段。

加载ELF格式的OS代码如下所示:

#define ELFHDR          ((struct elfhdr *)0x10000)      // 暂存空间

void
bootmain(void) {
    // 从硬盘读取一页大小数据(4K)到内存0x10000地址处
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

    // 通过ELF魔数检查是否为合法的ELF文件
    if (ELFHDR->e_magic != ELF_MAGIC) {
        goto bad;
    }

    struct proghdr *ph, *eph;

    // 加载每个程序头到内存中(忽略程序头的flags)
    ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);  //程序头表首项指针
    eph = ph + ELFHDR->e_phnum;  //程序头表尾指针,eph = ph + 3 * sizeof(struct proghdr)
    for (; ph < eph; ph ++) {
        // 依次将各个段加载到虚拟内存(此时虚拟内存 = 物理内存),内存地址固定在0x000000—0xFFFFFF之间
        readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
    }

    // 跳转到程序入口处执行,即0x100000处,(void (*)(void))addr为将addr(ELFHDR->e_entry &  
    // 0xFFFFFF)转换为无参数无返回值的函数指针,再调用函数指针执行该地址处的代码
    ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

bad:
    //内核文件不是ELF文件,死循环
    outw(0x8A00, 0x8A00);
    outw(0x8A00, 0x8E00);

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

参考

本项目github

以上是关于如何使用STM32F4的BootLoader和APP程序的主要内容,如果未能解决你的问题,请参考以下文章

如何将 STM32f4 编程为 SPI 从设备

如何使用STM32F4 的CCM RAM

如何设置快速 STM32 F4 FSMC 来控制 STM32F4Discovery 板上的显示?

如何在STM32F4中使用Backup SRAM作为EEPROM

如何使用STM32F4的DSP库

如何在 STM32F4 的 keil uvision 5.30 中“不支持命令”