QEMU裸机开发之打印字符
1.文件结构总览
先整体看下本章节裸机程序的工程文件组成,如下图所示。
- address.h:主要定义了一些外设寄存器的基址还有内存基址等。
- entry.S:入口文件,主要设置了栈。
- kernel.ld:编译用的链接脚本。
- Makefile:makefile文件,控制源码的编译过程。
- start.c:是C语言实现的入口,在entry.S中会调转到C函数中。
- uart.c和uart.h:串口驱动源码。
2.Makefile
Makefile控制着源码的编译,如下图所示。
# compile objects set KERNEL_IMAGE_NAME=kernelimage OBJS = \ entry.o \ start.o \ uart.o # cross compiler and flag set CROSS_COMPILER = riscv64-unknown-elf- CC = $(CROSS_COMPILER)gcc AS = $(CROSS_COMPILER)gas LD = $(CROSS_COMPILER)ld OBJCOPY = $(CROSS_COMPILER)objcopy OBJDUMP = $(CROSS_COMPILER)objdump CFLAGS = -Wall -Werror -O -fno-omit-frame-pointer -ggdb CFLAGS += -mcmodel=medany CFLAGS += -ffreestanding -fno-common -nostdlib -mno-relax CFLAGS += -I. CFLAGS += $(shell $(CC) -fno-stack-protector -E -x c /dev/null >/dev/null 2>&1 && echo -fno-stack-protector) # Disable PIE when possible (for Ubuntu 16.10 toolchain) ifneq ($(shell $(CC) -dumpspecs 2>/dev/null | grep -e '[^f]no-pie'),) CFLAGS += -fno-pie -no-pie endif ifneq ($(shell $(CC) -dumpspecs 2>/dev/null | grep -e '[^f]nopie'),) CFLAGS += -fno-pie -nopie endif LDFLAGS = -z max-page-size=4096 # compile kernel $(KERNEL_IMAGE_NAME): $(OBJS) kernel.ld $(LD) $(LDFLAGS) -T kernel.ld -o $(KERNEL_IMAGE_NAME) $(OBJS) $(OBJDUMP) -S $(KERNEL_IMAGE_NAME) > kernel.asm $(OBJDUMP) -t $(KERNEL_IMAGE_NAME) | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > kernel.sym clean: rm -f *.tex *.dvi *.idx *.aux *.log *.ind *.ilg \ $(OBJS) *.asm *.sym \ $(KERNEL_IMAGE_NAME) # qemu set QEMU = qemu-system-riscv64 ifndef CPUS CPUS := 1 endif QEMUOPTS = -machine virt -bios none -kernel $(KERNEL_IMAGE_NAME) -m 128M -smp $(CPUS) -nographic qemu: $(KERNEL_IMAGE_NAME) $(QEMU) $(QEMUOPTS)
这个makefile修改自xv6工程的makefile,需要注意的是qemu的启动命令,我们使用了“-bios none”选项,表示不使用OpenSBI这类的软件,而是直接加载我们编译的程序运行,所以我们写的程序开始就是运行在m模式下的。这里默认使用了128MB内存,并且使用单核方式。
3.链接脚本
链接脚本控制着整个程序的链接地址,如下图所示。
OUTPUT_ARCH( "riscv" ) ENTRY( _entry ) SECTIONS { /* * ensure that entry.S / _entry is at 0x80000000, * where qemu's -kernel jumps. */ . = 0x80000000; .text : { *(.text .text.*) . = ALIGN(0x1000); PROVIDE(etext = .); } .rodata : { . = ALIGN(16); *(.srodata .srodata.*) /* do not need to distinguish this from .rodata */ . = ALIGN(16); *(.rodata .rodata.*) } .data : { . = ALIGN(16); *(.sdata .sdata.*) /* do not need to distinguish this from .data */ . = ALIGN(16); *(.data .data.*) } .bss : { . = ALIGN(16); *(.sbss .sbss.*) /* do not need to distinguish this from .bss */ . = ALIGN(16); *(.bss .bss.*) } .stack (NOLOAD) : { . = ALIGN(16); PROVIDE (__stack_start = .); . += 128 * 1024; . = ALIGN(16); PROVIDE (__stack_end = .); } PROVIDE(end = .); }
整个程序链接地址是从0x80000000开始的,然后划分了5个段:代码段、只读数据段、数据段、清零数据段、栈段。栈段默认分配了128KB空间用于m模式和s模式下正常和异常时使用,这个在后面涉及到时会具体讲解。
4.入口处理
entry.S主要是设置了栈地址,因为我们使用的是模拟器,所以不需要初始化内存这类外设,只需要将栈设置好就可以跳转到c语言函数运行了,如下所示。
.section .text .global _entry _entry: # sp = __stack_start + ((hartid + 1) * 4096) la sp, __stack_start li a0, 4096 csrr a1, mhartid addi a1, a1, 1 mul a0, a0, a1 add sp, sp, a0 call start loop: j loop
这段代码参考自xv6,原来是可以设置多核下的栈的,因为现在我们只使用了单核,所以栈地址被设置为__stack_start+4096这个值,__stack_start就是在链接脚本里定义的,这个栈是m模式正常运行时所使用的栈,大小为4KB。栈设置完成之后,直接就调用c函数实现的start进行处理。
5.C语言主函数处理
strat.c中目前只有一个c语言实现的start函数,如下所示。
#include "uart.h" void start(void) { uart_puts("in start.\r\n"); }
目前的实现很简单,就是调用串口驱动发送字符串函数“uart_puts”打印一串信息。
6.串口驱动
我们qemu选用的平台是virt,使用的串口为16550兼容串口,由于是模拟器,我们可以直接实现发送函数就可以实现字符输出打印功能,而不需要去初始化,如下所示。
#include "address.h" // the UART control registers. // some have different meanings for // read vs write. // see http://byterunner.com/16550.html #define RHR 0 // receive holding register (for input bytes) #define THR 0 // transmit holding register (for output bytes) #define IER 1 // interrupt enable register #define IER_RX_ENABLE (1<<0) #define IER_TX_ENABLE (1<<1) #define FCR 2 // FIFO control register #define FCR_FIFO_ENABLE (1<<0) #define FCR_FIFO_CLEAR (3<<1) // clear the content of the two FIFOs #define ISR 2 // interrupt status register #define LCR 3 // line control register #define LCR_EIGHT_BITS (3<<0) #define LCR_BAUD_LATCH (1<<7) // special mode to set baud rate #define LSR 5 // line status register #define LSR_RX_READY (1<<0) // input is waiting to be read from RHR #define LSR_TX_IDLE (1<<5) // THR can accept another character to send // the UART control registers are memory-mapped // at address UART0. this macro returns the // address of one of the registers. #define Reg(reg) ((volatile unsigned char *)(UART0_REG_BASE + reg)) #define ReadReg(reg) (*(Reg(reg))) #define WriteReg(reg, v) (*(Reg(reg)) = (v)) // add a character to the output buffer and tell the // UART to start sending if it isn't already. void uart_putc(char c) { // wait for Transmit Holding Empty to be set in LSR. while((ReadReg(LSR) & LSR_TX_IDLE) == 0); WriteReg(THR, c); } void uart_puts(char *msg) { char c; if (!msg) { return; } while ((c = *msg) != '\0') { uart_putc(c); msg++; } }
逻辑也很简单,就是轮询等待发送寄存器为空,然后再写入数据发送,串口模块寄存器基址是定义在address.h中的,如下所示。
#ifndef QEMU_RISCV64_ADDRESS_H_ #define QEMU_RISCV64_ADDRESS_H_ // the kernel expects there to be RAM // for use by the kernel and user pages // from physical address 0x80000000 to PHYSTOP. #define KERNBASE 0x80000000L #define PHYSTOP (KERNBASE + 128*1024*1024) #define UART0_REG_BASE (0x10000000L) #define CLINT_REG_BASE (0x02000000L) #endif /* QEMU_RISCV64_ADDRESS_H_ */
其中CLINT的基址在后面设置核间中断和timer会用到,后面会讲解。
7.测试
在命令行输入“make qemu”即可编译,编译完成后会自动启动qemu运行程序,如果一切顺利会在最后看到“in start.”这个信息输出,如下所示。
gewenbin@gewenbin-virtual-machine:~/Desktop/qemu_test/lesson1$ make qemu riscv64-unknown-elf-gcc -c -o entry.o entry.S riscv64-unknown-elf-gcc -Wall -Werror -O -fno-omit-frame-pointer -ggdb -mcmodel=medany -ffreestanding -fno-common -nostdlib -mno-relax -I. -fno-stack-protector -fno-pie -no-pie -c -o start.o start.c riscv64-unknown-elf-gcc -Wall -Werror -O -fno-omit-frame-pointer -ggdb -mcmodel=medany -ffreestanding -fno-common -nostdlib -mno-relax -I. -fno-stack-protector -fno-pie -no-pie -c -o uart.o uart.c riscv64-unknown-elf-ld -z max-page-size=4096 -T kernel.ld -o kernelimage entry.o start.o uart.o riscv64-unknown-elf-objdump -S kernelimage > kernel.asm riscv64-unknown-elf-objdump -t kernelimage | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$/d' > kernel.sym qemu-system-riscv64 -machine virt -bios none -kernel kernelimage -m 128M -smp 1 -nographic in start. QEMU: Terminated gewenbin@gewenbin-virtual-machine:~/Desktop/qemu_test/lesson1$
如果想从qemu中退出到命令行,先按住“ctrl+a”,然后再按“x”键即可。输入“make clean”即可清零编译结果。
8. 工程源码
链接:https://pan.baidu.com/s/1TnTYr7mywdKj5bxpdmWnyA,提取码:q772,见lesson1。
评论