QEMU模拟ARM裸机搭建C运行环境

QEMU模拟ARM裸机搭建C运行环境

链接脚本

ENTRY(_start)

MEMORY {
    /* 0x0000 0000 - 0x0000 ffff */
    FLASH   (rx) : ORIGIN = 0x00000000, LENGTH = 64K
    /* 0x0001 0000 - 0x0801 0000 */
    RAM     (rw) : ORIGIN = 0x00010000, LENGTH = 128M
}

SECTIONS {
    .text : {					/* 未指定代码段LMA起始地址,默认LMA=VMA */
        KEEP(*(.init))			/* 保留启动入口(通常含 _start) */
        *(.text*)				/* 所有代码段 */
        *(.rodata*)				/* 只读数据(如字符串常量) */
    } > FLASH					/* VMA:运行地址在 FLASH 区 0x0000 0000 开始 */

    .data : AT(ADDR(.text) + SIZEOF(.text)) {	/* 指定数据块LMA */
        __data_load_addr = LOADADDR(.data);		/* 数据段在LMA的起始地址 = 代码段LMA起始地址(此处等于VMA起始地址) + 代码段长度 */
        __data_start = .;						/* 数据段在VMA的起始地址 */
        *(.data*)								/* 所有数据段 */
        __data_end = .;							/* 数据段在VMA的结束地址 */
    } > RAM										/* VMA 指定在RAM区 0x0001 0000 开始 */

    .bss : {									/* bss段默认不加载到LMA */
        __bss_start = .;						/* bss在VMA的起始地址 */
        *(.bss*)								/* 所有bss段 */
        __bss_end = .;							/* bss在VMA的结束地址 */
    } > RAM										/* VMA 指定在RAM区 接着data段 开始 */

    __stack_top = ORIGIN(RAM) + LENGTH(RAM);	/* 定义栈顶地址 */
}
  • MEMORY

    /* 
     * 裸机 ARM 启动链接脚本(用于 QEMU 仿真)
     * 内存模型:整个 256MB 物理内存从 0x00000000 开始,均为 RAM
     * 逻辑上划分为:
     *   - 低 64KB 作为“Flash”区域(存放代码和 .data 初始值)
     *   - 剩余空间作为运行时 RAM(存放 .data 副本、.bss、栈等)
     */
    
    • FLASH

          /* 
           * 逻辑“Flash”区域:64KB,从 0x00000000 开始
           * (rx) 表示可读、可执行(不可写,但实际是 RAM,写操作不会报错)
           */
      
    • RAM

          /* 
           * 运行时 RAM 区域:128M,从 0x00010000(64KB 之后)开始
           * (rw) 表示可读写
           */
      
  • SECTIONS

    /*
     * 段布局定义(SECTIONS 命令)
     * 控制各段在内存中的位置(VMA)和在镜像中的位置(LMA)
     */
    
    • .text

          /*
           * .text 段:包含可执行代码和只读数据
           * 放置在 FLASH 区域(即物理地址 0x00000000 起)
           * KEEP(*(.init)) 确保启动函数(如 _start)不会被优化掉
           */
      
    • .data

          /*
           * .data 段:已初始化的全局/静态变量
           * LMA(加载地址):紧跟 .text 之后,仍在 FLASH 区(即 bin 文件中)
           * VMA(运行地址):映射到 RAM 区,以便运行时可修改
           *
           * AT(...) 指定 LMA;> RAM 指定 VMA
           */
      
  • __stack_top

        /*
         * 定义栈顶地址
         * 栈从高地址向低地址生长,因此栈顶设为 RAM 区最高地址
         * 注意:ORIGIN(RAM) + LENGTH(RAM) = 0x0001 0000 + 128M = 0x0801 0000
         */
    

汇编启动文件

/* 
 * 裸机 ARM 启动代码(ARMv5T / arm926ej-s 兼容)
 * 功能:
 *   - 设置栈指针
 *   - 初始化 .data 段(从镜像复制到 RAM)
 *   - 清零 .bss 段
 *   - 跳转到 C 语言 main() 函数
 *
 * 依赖链接脚本中定义的符号:
 *   __stack_top, __data_load_addr, __data_start, __data_end,
 *   __bss_start, __bss_end
 */

/* 将 _start 放入 .init 段,确保它被链接到程序最开始 */
.section .init

/* 声明 _start 为全局符号,供链接器作为入口点使用 */
.global _start

@ 入口
_start:
    @ 设置栈指针
    ldr sp, =__stack_top

    @ 初始化.data区
    ldr r0, =__data_load_addr   @ 程序镜像中.data区
    ldr r1, =__data_start       @ RAM中.data区开始地址
    ldr r2, =__data_end         @ RAM中.data区结束地址

@ .data循环复制
data_loop:
    cmp r1, r2
    beq bss_init
    ldr r3, [r0], #4
    str r3, [r1], #4
    b data_loop

@ 初始化.bss区
bss_init:
    ldr r1, =__bss_start
    ldr r2, =__bss_end

@ .bss循环清空
bss_loop:
    cmp r1, r2
    beq call_main
    mov r3, #0
    str r3, [r1], #4
    b bss_loop

@ 调用main函数
call_main:
    bl main

@ main返回后死循环
halt:
    b halt

C代码

int c = 9;

int main(int argc, const char *argv[]) {
    int a = 10;
    int b = 20;

    return a + b + c;
}

Makefile

# =============================================================================
# ARM 裸机开发 Makefile(适用于 QEMU 仿真)
# 功能:
#   - 编译 C 和汇编源文件
#   - 链接生成 ELF 和 raw binary
#   - 支持反汇编、QEMU 仿真、GDB 调试
# =============================================================================

# ===== 工具链 =====
# 指定交叉编译工具链前缀
CROSS_COMPILE = arm-none-eabi-
CC      = $(CROSS_COMPILE)gcc
AS		= $(CROSS_COMPILE)as
LD      = $(CROSS_COMPILE)ld
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump
GDB     = $(CROSS_COMPILE)gdb

# ===== 目标 =====
TARGET  = main

# ===== 源文件 =====
# C 源文件(主程序)
SRCS_C  = main.c
# 汇编源文件
SRCS_S  = startup.S

# ===== 中间目标文件 =====
# 自动将 .c → .o,.S → .o
OBJS = $(SRCS_C:.c=.o) $(SRCS_S:.S=.o)

# ===== 编译选项 =====
# gcc参数
CFLAGS = \ 
	-nostdlib  \ 		# 不链接标准 C 库(裸机无 libc)
	-ffreestanding \ 	# 告诉编译器这是独立环境(可省略某些内建假设)
	-g \ 				# 生成调试信息(供 GDB 使用)
	-mcpu=arm9e \ 		# 指定 CPU 架构(arm926ej-s 属于 arm9e)
	-Wall				# 启用所有警告
# as参数
ASFLAGS = \ 
		-g \			# 生成调试信息 
		-mcpu=arm9e		# 与 C 一致的 CPU 架构

# ===== 链接脚本 =====
LDSCRIPT = linker.ld

# ===== 默认目标 =====
# 执行 `make` 时,默认构建 bin 和 dump 文件
main: $(TARGET).bin $(TARGET).dump

# ===== 编译规则 =====
# 通用规则:将 .c 文件编译为 .o
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# 通用规则:将 .S 文件汇编为 .o
%.o: %.S
	$(AS) $(ASFLAGS) -c $< -o $@

# ===== 链接 ELF =====
$(TARGET).elf: $(OBJS)
	$(LD) -T $(LDSCRIPT) -o $@ $^

# ===== ELF → BIN =====
$(TARGET).bin: $(TARGET).elf
	$(OBJCOPY) -O binary $< $@

# ===== 反汇编 =====
$(TARGET).dump: $(TARGET).elf
	$(OBJDUMP) -d $< > $@


# ===== QEMU =====
QEMU = qemu-system-arm
QEMU_OPTS = \
	-M none \				# 不使用任何预设开发板(纯自定义内存)
	-cpu arm926 \			# 指定 CPU 型号(arm926ej-s 的简称)
	-nographic \			# 禁用图形界面
	-device loader,addr=0x00000000,file=$(TARGET).bin \		# 从 0x0 加载 bin
	-m 256M \				# 分配 256MB 连续 RAM(从 0x0 开始)
	-S \ 					# 启动时暂停(-S)
    -s						# 开启 GDB 远程调试默认端口1234

# ===== 运行 QEMU =====
qemu: $(TARGET).bin
	sudo $(QEMU) $(QEMU_OPTS)

# ===== 运行GDB =====
gdb:
	$(GDB) $(TARGET).elf

# ===== 清理 =====
clean:
	rm -f *.o *.elf *.bin *.dump

# ===== 声明伪目标(phony targets)=====
# 防止与同名文件冲突
.PHONY: main clean qemu gdb
posted @ 2025-12-26 10:55  L-ZH  阅读(0)  评论(0)    收藏  举报