打印
[其他]

Cortex-M3 ARM代码编译,链接与启动过程深度分析

[复制链接]
3116|61
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
本篇文章以武汉杰开科技的汽车级MCU芯片AC7811为硬件平台,使用GNU GCC作为开发工具。详细分析Compile 、Link 、Loader的过程以及Image(二进制程序)启动的详细分析。整个过程分析涉及到RW可读写DATA段从Flash到Mem的Copy,BSS段的初始化,Stack和Heap的初始化,C库函数移植、利用Semihosting 实现基本的IO等内容。基本可以让你从更深刻的层面理解源码 -> 编译 -> 链接 -> 运行的整个过程。理解了这些个过程之后,你就对那些从语言编程层面来说难于理解的问题自然领会了,比如:我们的源码时如何生成相应的代码段和数据段,代码段和数据段在哪?全局变量和局部变量的区别到底在哪?Stack和Heap的区别到底在哪?等等一些看起来是规定的东西,书本里一切不自然的概念都需要你用心去理解,去实践,达到自然的状态才有可能去解决实际遇到的问题。本文参考官方文档:Makefile,GNU GCC,Linkers and Loaders,Cortex-M3 Technical Reference Manual和程序员的自我修养—链接、装载与库。

使用特权

评论回复
评论
米多0036 2022-12-31 13:27 回复TA
———————————————— 版权声明:本文为CSDN博主「背包旅行码农」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/zhuwade/article/details/121944254 

相关帖子

沙发
米多0036|  楼主 | 2022-12-30 23:57 | 只看该作者
AUTOChip Cortex-M3 地址映射(Memory mapping)
​ 如在我们比较常用的操作系统(Windows/Linux)中,整个Virtual Memory地址通过硬件的MMU被映射到相应的Physical RAM/Memory。然而在嵌入式系统中对于RAM,它时没有MMU的。因此在一些嵌入式系统里,如比较常用的STM32来说,地址映射被分为Flash Segments(也是我们所知的Flash,用来存储代码和数据)和RAM Segments用来存可读写数据。在分析代码编译,链接与启动之前,我们必须了解整个Cortex-M3 MCU的地址分配和启动模式。

使用特权

评论回复
板凳
米多0036|  楼主 | 2022-12-30 23:58 | 只看该作者

使用特权

评论回复
地板
米多0036|  楼主 | 2022-12-30 23:59 | 只看该作者
从上图中AutoChip AC7811的Flash和SRAM地址范围分别在0x000 0000 ~ 0x2000 0000和0x2000 0000 ~ 0x4000 0000,内部外围总线地址0x4000 0000 ~ 0x6000 0000(这是我们常用的说的_APB_(Advanced Peripheral Bus),外围总线地址),外部存储设备地址0x6000 0000 ~ 0x6100 0000(这里是给外挂SPI FLASH映射的地址空间),Cortex-M3私有Debug,外部和内部总线接口地址0xE000 0000 ~ 0xE100 0000(参考Cortex-M3 Technical Reference Manual)。本文主要讲的是Flash地址0x000 0000 ~ 0x2000 0000和SRAM地址0x2000 0000 ~ 0x4000 0000的应用。

使用特权

评论回复
5
米多0036|  楼主 | 2022-12-30 23:59 | 只看该作者
下面通过分析AutoChip AC7811的四个boot mode启动模式帮助大家理解对应的地址映射转换。在AC7811技术参考手册page23 Boot configuration可以设置UART1_CTS和UART1_RTS管脚使能不同的启动模式。

使用特权

评论回复
6
米多0036|  楼主 | 2022-12-31 13:15 | 只看该作者

使用特权

评论回复
7
米多0036|  楼主 | 2022-12-31 13:16 | 只看该作者
Flash memory boot up 和 SRAM boot up映射框图如下:

使用特权

评论回复
8
米多0036|  楼主 | 2022-12-31 13:18 | 只看该作者
ISP boot up 和 Serial flash boot up映射框图如下:

使用特权

评论回复
9
米多0036|  楼主 | 2022-12-31 13:19 | 只看该作者

使用特权

评论回复
10
米多0036|  楼主 | 2022-12-31 13:20 | 只看该作者
裸机程序的整体说明
​ 我们都熟悉有操作系统支持的应用程序开发,比如 Linux下C语言的开发。我们可以不用关心程序启动的细节,同时我们一般还可以使用各种方便的lib 库,比如基本的IO操作(printf scan),动态分配内存操作(malloc),文件操作(fopen fwrite fread)等。有操作系统支持的情况下,程序的编译、链接、启动都是有操作系统支持的,常用的编程库函数使用的是标准的C库。

使用特权

评论回复
11
米多0036|  楼主 | 2022-12-31 13:25 | 只看该作者
那如果没有OS支持的情况下,想实现上面这些功能的话,该怎么做呐?这种情况就叫 Bare Metal (裸)程序开发。在嵌入式开发中是比较常见的情况,本文主要讲解基于Cortex-M3 的裸程序开发。本裸机程序实现了 基本IO,动态内存分配,基本函数库等功能。

使用特权

评论回复
12
米多0036|  楼主 | 2022-12-31 13:27 | 只看该作者
如何实现的startup
​ 还是和有OS支持的情况下来对比,有OS的情况下分析一个Project,一般会从3个方面来进行分析:一是看源代码的组织形式;一是看Compile && Link过程(即Makefile);三是看Run时的情况(一般看运行起来后几个Process,几个Thread,以及他们之间的关系)。分析完这3个方面后,整个project从静到动,以及动静之间的转换都包括了,也就掌握了整个的Project。

使用特权

评论回复
13
米多0036|  楼主 | 2022-12-31 13:28 | 只看该作者
在没有OS的情况下,1 2 两个方面是一样的,只不过程序运行的基础环境不一样,裸机程序运行需要考虑的细节多一些。裸机程序需要考虑的基本问题有:

编译生成的可执行程序结构是什么样的?整个可执行程序的入口在哪?

需要将可执行程序下载到什么地方?程序运行前需要做哪些准备工作?

C语言运行需要什么样的环境?

我们按照上面说的方法,从3各方面出发,分析我们的Project。

使用特权

评论回复
14
米多0036|  楼主 | 2022-12-31 13:29 | 只看该作者
源代码:
顶层目录结构:
# tree -l
.
├── App
├── Device
│   ├── Include
│   │   ├── CMSIS
│   └── Source
│       ├── ARM
├── Drivers
│   ├── inc
│   └── src
└── makefile

使用特权

评论回复
15
米多0036|  楼主 | 2022-12-31 13:31 | 只看该作者
其中App目录是Application层的主逻辑代码,其中main.c就在App目录中,是业务逻辑层的主代码。 Device-->include-->CMSIS目录是arm cmsis框架层的interface 说明文件。 Device-->Source-->ARM目录是启动代码startup_ac78xx.s和ac7811_flash.ld脚本。 ac7811_flash.ld脚本主要告诉ld(链接器)如何链接各个Objects文件为可执行程序. Drivers目录是AutoChip提供的ac7811的SDK包。

使用特权

评论回复
16
米多0036|  楼主 | 2022-12-31 13:31 | 只看该作者
更详细的项目目录结构:
# tree -l
.
├── App
│   └── main.c
├── Device
│   ├── Include
│   │   ├── CMSIS
│   │   │   ├── arm_common_tables.h
│   │   │   ├── arm_const_structs.h
│   │   │   ├── arm_math.h
│   │   │   ├── cmsis_armcc.h
│   │   │   ├── cmsis_armclang.h
│   │   │   ├── cmsis_compiler.h
│   │   │   ├── cmsis_gcc.h
│   │   │   ├── cmsis_iccarm.h
│   │   │   ├── cmsis_version.h
│   │   │   ├── core_cm3.h
│   │   │   ├── mpu_armv7.h
│   │   ├── ac78xx.h
│   │   ├── ac78xx_ckgen.h
│   │   ├── ac78xx_debugout.h
│   │   ├── ac78xx_spm.h
│   │   ├── debugzone.h
│   │   └── system_ac78xx.h
│   └── Source
│       ├── ARM
│       │   ├── ac7811_flash.ld
│       │   └── startup_ac78xx.s
│       ├── ac78xx_ckgen.c
│       ├── ac78xx_ckgen_regs.h
│       ├── ac78xx_debugout.c
│       ├── ac78xx_spm.c
│       ├── ac78xx_spm_regs.h
│       ├── syscalls.c
│       └── system_ac78xx.c
├── Drivers
│   ├── inc
│   │   ├── ac78xx_can.h
│   │   ├── ac78xx_can_reg.
│   │   ├── ac78xx_dma.h
│   │   ├── ac78xx_dma_reg.h
│   │   ├── ac78xx_eflash.h
│   │   ├── ac78xx_eflash_reg.h
│   │   ├── ac78xx_uart.h
│   │   ├── ac78xx_uart_reg.h
│   │   ├── ......
│   └── src
│       ├── ac78xx_can.c
│       ├── ac78xx_dma.c
│       ├── ac78xx_eflash.c
│       ├── ac78xx_uart.c
│       ├── ......
└── makefile

使用特权

评论回复
17
米多0036|  楼主 | 2022-12-31 13:32 | 只看该作者
编译与链接
当然是直接 make 喽。但是我们还是需要知道对应的编译规则。不得不说AC7811的makefile写的还是非常规范的。我们可以把作为一个很好的makefile模版。

使用特权

评论回复
18
米多0036|  楼主 | 2022-12-31 13:33 | 只看该作者
makefile详细说明:
#--------------------------------- 编译参数 ------------------------------------
#把编译过程中的命令参数log不往屏幕显示
ifneq ($(V),1)
Q                := @
NULL        := 2>/dev/null
endif

TARGET := DEMO#编译文件名称,根据命名需要可自行修改
OPT    := -O0#不做任何优化,这是默认的编译选项。
CSTD   := -std=c11#使用C11标准库
CXXSTD := -std=c++11#使用C++11标准库

#--------------工程需要编译的头文件,根据需要自行添加--------------------
INC_FLAGS += -I ./Device/Include     \
                     -I ./Device/Include/CMSIS \
                         -I ./Drivers/inc       
#------链接文件,里面指定了芯片flash,ram大小,需根据实际大小进行修改
LDSCRIPT := ./Device/Source/ARM/ac7811_flash.ld

ARCH_FLAGS += -mthumb#thumb指令
ARCH_FLAGS += -mcpu=cortex-m3#cortex-m3 cpu架构

#编译告警设置
CWARN_FLAGS += -Wall -Wshadow
CWARN_FLAGS += -fno-common -ffunction-sections -fdata-sections
CWARN_FLAGS += -Wimplicit-function-declaration  
CWARN_FLAGS += -Wstrict-prototypes

#--通过printf打印串口log,需设置-specs=nosys.specs,并且在syscalls.c中实现_write_r函数,把printf映射到串口上。
LDLIBS                += -Wl,--start-group -lc -lgcc  -Wl,--end-group -lm -specs=nosys.specs

#----------------------------- 搜索工程目录下的源代码 ---------------------------

AS_SRC := ./Device/Source/ARM/startup_ac78xx.s
AS_OBJ := $(AS_SRC:%.s=%.o)
#-------源代码需根据实际情况删减-------------
C_SRC := ./Device/Source/ac78xx_ckgen.c    \
                 ./Device/Source/ac78xx_spm.c      \
                 ./Device/Source/system_ac78xx.c   \
                 ./Device/Source/ac78xx_debugout.c \
                 ./Device/Source/syscalls.c \
                 ./Drivers/src/ac78xx_dma.c        \
                 ./Drivers/src/ac78xx_gpio.c        \
                 ./Drivers/src/ac78xx_timer.c        \
                 ./Drivers/src/ac78xx_uart.c        \
                 ./Drivers/src/ac78xx_wdg.c        \
                 ./App/main.c
C_OBJ := $(C_SRC:%.c=%.o)  

#--------------------------------- 参数整合 ------------------------------------
# C flags
CFLAGS := $(OPT) $ $(CSTD) $(INC_FLAGS) $(FP_FLAGS)
CFLAGS += $(DEFINES) $(ARCH_FLAGS) $(CWARN_FLAGS) -g #-g 增加调试选项,可以使用GDB进行调试

# Linker flags 链接器编译选项
LDFLAGS                := --static#静态编译
LDFLAGS                += -Wl,-Map=$(TARGET).map -Wl,--gc-sections
LDFLAGS                += -T$(LDSCRIPT) $(ARCH_FLAGS) $(LDLIBS)

# OBJ
OBJ = $(AS_OBJ) $(C_OBJ)

#-------------------------------- 编译器调用指令 --------------------------------
PREFIX        := arm-none-eabi

CC                := $(PREFIX)-gcc
CXX                := $(PREFIX)-g++
LD                := $(PREFIX)-gcc
AR                := $(PREFIX)-ar
AS                := $(PREFIX)-as
OBJCOPY        := $(PREFIX)-objcopy
OBJDUMP        := $(PREFIX)-objdump
GDB                := $(PREFIX)-gdb

.SUFFIXES: .elf .bin .hex .list .map .images
.SECONDEXPANSION:
.SECONDARY:

all: elf bin hex

elf: $(TARGET).elf
bin: $(TARGET).bin
hex: $(TARGET).hex
list: $(TARGET).list
images: $(TARGET).images

%.images: %.bin %.hex %.list %.map
        [url=home.php?mod=space&uid=384087]@printf[/url] "*** $* images generated ***\n"
#objdump生成二进制文件
%.bin: %.elf         
        @printf "  OBJCOPY $(*).bin\n"
        $(Q)$(OBJCOPY) -Obinary $(*).elf $(*).bin
#objdump生成hex文件
%.hex: %.elf
        @printf "  OBJCOPY $(*).hex\n"
        $(Q)$(OBJCOPY) -Oihex $(*).elf $(*).hex
       
%.list: %.elf
        @printf "  OBJDUMP $(*).list\n"
        $(Q)$(OBJDUMP) -S $(*).elf > $(*).list
#链接map生成elf规则       
%.elf %.map: $(OBJ) $(LDSCRIPT)
        @printf "  LD      $(TARGET).elf\n"
        $(Q)$(LD) $(OBJ) $(LDFLAGS) -o $(TARGET).elf
#汇编文件编译规则
$(AS_OBJ): %.o:%.s
        @printf "  AS      $(*).s\n"
        $(Q)$(CC) $(ARCH_FLAGS) $(FP_FLAGS) -g -Wa,--no-warn -x assembler-with-cpp -o $(*).o -c $(*).s
#C文件编译规则
$(C_OBJ): %.o:%.c
        @printf "  CC      $(*).c\n"
        $(Q)$(CC) $(CFLAGS) -o $(*).o -c $(*).c
       
clean:
        @#printf "  CLEAN\n"
        $(Q)$(RM) $(shell find -name '*.o' -o -name '*.d' -o -name '*.elf' -o -name '*.bin')
        $(Q)$(RM) $(shell find -name '*.hex' -o -name '*.srec' -o -name '*.list' -o -name '*.map')
        $(Q)$(RM) $(shell find -name 'generated.*' -o -name '*.srec' -o -name '*.list' -o -name '*.map')

.PHONY: images clean elf bin hex list flash debug

使用特权

评论回复
19
米多0036|  楼主 | 2022-12-31 13:33 | 只看该作者
Makefile Tips
(1)常用的变量名(约定俗成的):
CC:表示c编译器版本
CFLAGS:表示编译时参数
CPPFLAGS:表示预处理参数
CXX:表示C++编译器版本
CXXFLAGS:表示c++编译时参数
LDFLAGS:表示库参数库选项
INCLUDE:表示头文件目录
TARGET:表示目标名
RM:删除选项
#: 注释符号
(2)一些特殊字符
$(变量):对变量取值
@:只显示命令结果,忽略命令本身
-:如果当前命令出错,忽略错误,继续执行
%:通配符,通配符是以遍历的方式实现的
(3)特殊变量
用于当前目标:
$@:代表目标
$<:代表依赖中的第一个
$^:代表所有依赖

使用特权

评论回复
20
米多0036|  楼主 | 2022-12-31 13:34 | 只看该作者
映像结构与运行
​ 有操作系统的情况下,我们不需要关心可执行映像的具体结构,一个可执行程序文件从静态文件到动态运行这个过程叫Loader&&Run。这个过程是由OS来完成的,应用程序级别的开发是不需要关心这些细节的。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

97

主题

1335

帖子

0

粉丝