1、准备二进制运行程序镜像
利用 QEMU 仿真一个完整的系统,并创建最简单的“Hello world!”示例。
QEMU 模拟器支持 VersatilePB 平台,该平台包含一个 ARM926EJ-S 核心,以及其他外设,四个 UART 串行端口;特别是第一个串行端口(UART0)在使用 -nographic 或 “-serial stdio” qemu 选项时充当终端。UART0 被映射的地址:0x101f1000。
为了实现简单的“Hello world!”打印,编写test.c 文件如下:
volatile unsigned int * const UART0DR = (unsigned int *)0x101f1000;void print_uart0(const char *s) {while(*s != '\0') { /* 循环直到字符串结束 */*UART0DR = (unsigned int)(*s); /* 传输字符 */s++; /* 下一个字符 */}
}void c_entry() {print_uart0("Hello world!\n");
}
代码非常简单;一些细节:
-
volatile 关键字是必要的,以指示编译器 UART0DR 指向的内存可以独立于程序改变或产生影响。 unsigned int类型强制执行 32 位读写访问。
-
QEMU 模型的 PL011 串行端口忽略了传输 FIFO 功能;在实际的系统芯片中,必须在
UARTFR 寄存器中检查“传输 FIFO 满”标志,然后才在 UARTDR 寄存器上写入。 -
-kernel 选项将二进制文件(通常是 Linux 内核)加载到系统内存中,从地址0x00010000 开始。模拟器从地址 0x00000000 开始执行,其中一些指令(已经就位)用于跳转到内核映像的开头。ARM核心的中断表通常位于地址 0x00000000。
startup.s 汇编器文件内容如下:
.global _Reset
_Reset:LDR sp, =stack_topBL c_entryB .
链接器脚本 test.ld:
ENTRY(_Reset)
SECTIONS
{. = 0x10000;.startup . : { startup.o(.text) }.text : { *(.text) }.data : { *(.data) }.bss : { *(.bss COMMON) }. = ALIGN(8);. = . + 0x1000; /* 4kB 的堆栈内存 */stack_top = .;
}
运行的命令,生成相应的elf和bin文件:
$ arm-none-eabi-as -mcpu=arm926ej-s -g startup.s -o startup.o
$ arm-none-eabi-gcc -c -mcpu=arm926ej-s -g test.c -o test.o
$ arm-none-eabi-ld -T test.ld test.o startup.o -o test.elf
$ arm-none-eabi-objcopy -O binary test.elf test.bin
2、执行和调试二进制文件
在模拟器中运行程序,命令是:
$ qemu-system-arm -M versatilepb -m 128M -nographic -kernel test.bin
-M 选项指定了被模拟的系统。程序在终端打印 “Hello world!” 并无限期运行;要退出 QEMU,请按 “Ctrl + a” 然后按 “x”。
QEMU 实现了一个使用 TCP 连接的 gdb 连接器。按照以下方式运行模拟器:
$ qemu-system-arm -M versatilepb -m 128M -nographic -s -S -kernel test.bin
此命令在执行任何客户代码之前冻结系统,并在 TCP 端口 1234 上等待连接。从另一个终端,我运行 arm-none-eabi-gdb 并输入命令:
target remote localhost:1234
file test.elf
这连接到 QEMU 系统并加载测试程序的调试符号,其二进制图像已经加载在系统内存中。从那里,可以使用 continue 命令运行程序,单步执行程序并进行一般调试。gdb 中的 exit 命令关闭了调试器和模拟器。
编译和运行命令的参考结果见下图,左边是编译和执行的情况,右边是用arm-none-eabi-gdb工具调试的情况: