ARM 基础学习记录 / ARM 裸机编程

汇编程序调用 C 程序详情

在 C 程序和 ARM 汇编程序之间相互调用时必须遵守 ATPCS 规则,其是基于 ARM 指令集和 THUMB 指令集过程调用的规范,规定了调用函数如何传递参数,被调用函数如何获取参数,以何种方式传递函数返回值。

  1. 寄存器 R0~R15 在 ATPCS 规则的使用

    • 在函数中,通过寄存器 R0~R3 来传递参数,被调用的函数在返回前无需恢复寄存器 R0~R3 的内容。

    • 在函数中,通过寄存器 R4~R11 来保存局部变量。

    • 寄存器 R12 用作函数间 scratch 寄存器。

    • 寄存器 R13 用作栈指针,记作 SP ,在函数中寄存器 R13 不能用做其他用途,寄存器 SP 在进入函数时的值和退出函数时的值必须相等。

    • 寄存器 R14 用作链接寄存器,记作 LR ,它用于保存函数的返回地址,如果在函数中保存了返回地址,则 R14 可用作其它的用途。

    • 寄存器 R15 是程序计数器,记作 PC ,它不能用作其他用途。

  2. 汇编程序向 C 程序函数传递参数

    • 当参数小于等于 4 个时,使用寄存器 R0~R3 来进行参数传递。

    • 当参数大于 4 个时,前四个参数按照上面方法传递,剩余参数传送到栈中,入栈的顺序与参数顺序相反,即最后一个参数先入栈。

  3. C 程序函数返回结果给汇编程序

    • 结果为一个 32 位的整数时,通过寄存器 R0 返回。

    • 结果为一个 64 位整数时,通过 R0 和 R1 返回,依此类推。

    • 结果为一个浮点数时,通过浮点运算部件的寄存器 f0,d0 或 s0 返回。

    • 结果为一个复合的浮点数时,通过寄存器 f0-fN 或者 d0~dN 返回。

    • 对于位数更多的结果,通过调用内存来传递。

  4. 当 C 程序从一个函数跳转到另一个函数时,会先把源函数的 CPU 的寄存器和函数内的局部变量都入栈,当跳回时再出栈,这一过程的汇编代码是当 C 程序编译成汇编时被编译器自动添加。


imx6ull 裸机编程相关

这里是处理器启动流程等的介绍,属于科普环节,有个印象,会加深对于处理器如何运行的理解,非必要记住,而是为以后的操作说明每一个步骤都在做什么事情。此部分理解为主。

裸机映像文件合成详情

先说原理,看 imx6ull 芯片手册可知,芯片上电时内部的 boot ROM 固化的程序会通过外部引脚确定启动方式(USB\NAND\EMMC\SD等),将应用的二进制数据(app.bin)从存储区(NAND\EMMC\SD等)搬运到内存区(DDR2\3等),然后跳转到内存区的程序处开始执行程序。这个过程是这个芯片自动完成的,但是需要根据规定合成烧录到存储区的映像文件, 在编译得到应用的二进制文件 app.bin(这个就是比如 裸机应用固件 或 Linux 固件等)之后,再用 mkimage 工具(gcc-arm-linux-gnueabihf-6.2.1 编译器自带的)根据 imximage.cfg.cfgtmp 这个文件的信息,合成头部信息,再与 app.bin 组合生成 .imx 文件, .imx 的头部再添加 1KB 的数据(可以全为0,也可包含分区表等数据) 组合生成 .img 文件,具体如下:

  • .imx 文件 = 头部信息( IVT + Boot data + DCD) + app.bin -> 用于在烧写工具中烧写到 EMMC 中,烧写工具会自动将其烧写到 1KB 偏移处。

  • .img 文件 = 1k.bin + .imx 文件 = 1k.bin + 头部信息( IVT + Boot data + DCD) + app.bin -> 用于在烧写工具中烧写到 SD 中,烧写工具会将其烧写到 0 位置处(对与 SD 的烧写,此工具不会自动加 1KB 偏移...)。

头部信息包含了指示 boot ROM 程序要把 app.bin 数据搬运到内存的何处,其大小,以及包含了配置 DDR 的寄存器、引脚等数据等待,具体如下:

  • IVT:Image vector table,含 header(含 tag、length、version,这 3 项,length 表示 IVT 的大小)、entry(指示 app.bin 在内存中的位置,即程序数据被复制到内存哪里)、dcd(指示 DCD 数据 在内存中的位置)、boot_data(指示 Boot data 在内存中的位置)、self(指示 IVT 在内存中的位置)等,共占 32*8bit 大小,entry 为 app.bin 要在内存中的目的地址。

  • Boot data:start(映像文件在内存中的地址,为 IVT 在内存中的绝对地址减去 1024 偏移)、length(整个映像文件的长度,含 1k.bin)、plugin,共占 32*3bit 大小。

  • DCD:配 imx6ull 芯片的寄存器,如 DDR 的配置等,可自定,复杂,mkimage 根据 imximage.cfg.cfgtmp 这个文件的信息合成。

    其中,entry(指示 app.bin 在内存中的位置,即程序数据被复制到内存哪里)的地址在 Makefile 中调用 mkimage 工具时是可以指定的,在"重定位"章节会细说。

具体分布:

  • 头部数据和偏移区使用 mkimage 工具生成,官方都会提供的。

  • 最前面的灰色部分就是偏移数据区,对于EMMC/SD存储区设备是 1KB,对于 NAND 是256B,具体看手册。

最终生成的 .img 文件结构:

imx6ull 上电启动过程分析:

  1. boot Rom 会把 EMMC 或 SD 卡的前 4K 数据(涵盖了头部信息( IVT + Boot data + DCD)这些等)读入到芯片内部 RAM 运行。

  2. boot Rom 根据 DCD 进行初始化 DDR。

  3. boot Rom 根据 IVT,从 EMMC 或 SD 卡中将 app.bin 读到 DDR 的 0x80100000 地址(IVT 的 entry,如上图所示)。

  4. 跳转到 DDR 的 0x80100000 地址执行,即 CPU 开始从内存 0x80100000 地址开始执行机器码。

    以上步骤执行完之后的 DDR 内存图示:(这是反汇编 应用固件 产生的 机器码-汇编码 相互对应的内容)

重定位、启动和编译

各段数据重排序

每一个汇编成机器码的 .o 文件都会分为这几个数据段:

  • 代码段(.text):存放代码指令;

  • 只读数据段(.rodata):存放有初始值并且 const 修饰的全局类变量;

  • 数据段(.data):存放有初始值的全局类变量(有非零初始值的变量,如 char A = 'A';);

  • 零初始化段(.bss):存放没有初始值或初始值为0的全局类变量(如 int g_intA = 0;int g_intB;,这些存放在 .bss 段);

  • 注释段(.comment):存放注释。

在 Makefile 文件中,在链接步骤,通过 LD 工具,把各个 .o 文件的各个数据段,按照 imx6ull.lds 定义的顺序安放,即各段数据重排序,最后合成一个二进制文件 app.bin,其中的代码段(.text)、只读数据段(.rodata)和数据段(.data)等都来自于前面各个 .o 文件,每个段 的顺序按照 imx6ull.lds 安放。

链接脚本 imx6ull.lds 解析(一体式链接脚本格式):

复制代码
SECTIONS {
    . = 0x80100000;                      //设定链接地址为0x80100000
​
    . = ALIGN(4);                        //将当前地址以4字节为标准对齐
    .text      :                         //创建段,其名称为 .text
    {                                    //.text包含的内容为所有链接文件的数据段
        *(.text)                         // *:表示所有文件
    }
​
    . = ALIGN(4);                        //将当前地址以4字节为标准对齐
    .rodata : { *(.rodata) }             //.rodata存放在.text之后,包含所有链接文件的只读数据段
​
    . = ALIGN(4);
    .data : { *(.data) }                 //.data存放在.rodata之后,包含所有链接文件的只读数据段
​
    . = ALIGN(4);
    __bss_start = .;                     //将当前地址的值存储为变量__bss_start
    .bss : { *(.bss) *(.COMMON) }        //.bss存放在.data段之后, 包含所有文件的bss段和注释段
    __bss_end = .;                       //将当前地址的值存储为变量__bss_end
}
​

可见 imx6ull.lds 文件给出 .bss 段的头、尾地址标识:__ bss_start__ bss_end

启动文件程序

以最简单的裸机点灯程序的启动文件 start.S 为例。仅为示例,过于简单,完整示例可看 下面 "ARM异常处理 & 启动文件的示例" 一节。

复制代码
.text
.global  _start
_start:                 
    /* 设置栈地址 */
    ldr  sp,=0x80200000
    bl main
​
halt:
    b  halt
Makefile 文件解析

以最简单的裸机点灯程序的 makefile 为例。

复制代码
PREFIX=arm-linux-gnueabihf-
CC=$(PREFIX)gcc
LD=$(PREFIX)ld
AR=$(PREFIX)ar
OBJCOPY=$(PREFIX)objcopy
OBJDUMP=$(PREFIX)objdump
​
led.img : start.S  led.c main.c
    $(CC) -nostdlib -g -c -o start.o start.S                 # 把启动文件 .s 和各个 .c 文件都汇编为机器码文件 .o
    $(CC) -nostdlib -g -c -o led.o led.c    
    $(CC) -nostdlib -g -c -o main.o main.c  
    
    $(LD) -T imx6ull.lds -g start.o led.o main.o -o led.elf  # 链接,按照 imx6ull.lds 定义的格式,各段数据重排序,把各个 .o 文件组成 .elf 文件
    
    $(OBJCOPY) -O binary -S led.elf  led.bin                 # .elf 转为 .bin 二进制文件,应用二进制文件
    $(OBJDUMP) -D -m arm  led.elf  > led.dis    
    mkimage -n ./tools/imximage.cfg.cfgtmp -T imximage -e 0x80100000 -d led.bin led.imx
                                                             # 使用 mkimage 生成 头部数据,并与 .bin 组合,产生 .imx 文件
    dd if=/dev/zero of=1k.bin bs=1024 count=1                # 创建一个 1KB 的空文件 1k.bin
    cat 1k.bin led.imx > led.img                             # 把 1k.bin 放在 .imx 前头,组合成 .img 文件
​
clean:
    rm -f led.dis  led.bin led.elf led.imx led.img *.o
​
清零 bss 段

在 启动文件 汇编程序中,根据 .bss 段的头、尾地址(__ bss_start__ bss_end)来对此区域清零,让 C 程序中未定义初始值或零初始值的变量在初始化时都为零值,而非随机值。

附程序:

复制代码
clean_bss:
    ldr r1, =__bss_start    @ 将链接脚本变量__bss_start变量保存于r1
    ldr r2, =__bss_end      @ 将链接脚本变量__bss_end变量保存于r2
    mov r3, #0
clean:
    strb r3, [r1]           @ 将当前地址下的数据清零
    add r1, r1, #1          @ 将r1内存储的地址+1
    cmp r1, r2              @ 相等:清零操作结束;否则继续执行clean函数清零bss段
    bne clean
    
    mov pc, lr

并在进入主函数前调用 bl clean_bss /* 清零bss段 */

数据段再单独重定位

事出有因,想要把 .data 段的数据放到 片内内存中以加快访问速度,参考芯片手册得到片内RAM的地址为:0x900000 ~ 0x91FFFF,共128KB(当然不会很大,也就裸机下的编一编、学一学行,Linux 系统等的大型工程就不适合了),所以我们将 .data 段重定位后的地址设置为0x900000。

第一步:把链接脚本 imx6ull.lds 中的 .data : { *(.data) }换成下面的:

复制代码
     data_load_addr = .;                    
     .data 0x900000 : AT(data_load_addr) 
     {
       data_start = . ;                  //addr = 0x900000
       *(.data)
       data_end = . ;                    //addr = 0x900000 + SIZEOF(.data)
     }

第二步:在启动文件中,复制 data 段数据到片内内存 data_start

复制代码
 copy_data:
      /* 重定位data段 */
      ldr r1, =data_load_addr     /* data段的加载地址, 从链接脚本中得到, 0x8010xxxx */
      ldr r2, =data_start        /* data段重定位地址, 从链接脚本中得到, 0x900000 */
      ldr r3, =data_end          /* data段结束地址, 从链接脚本中得到,0x90xxxx */
 cpy:
      ldr r4, [r1]              /* 从r1读到r4 */
      str r4, [r2]              /* r4存放到r2 */
      add r1, r1, #4           /* r1+1 */
      add r2, r2, #4           /* r2+1 */
      cmp r2, r3               /* r2 r3比较 */
      bne cpy                  /* 如果不等则继续拷贝 */
​
      mov pc, lr               /* 跳转回调用copy_data函数之前的地址 */

并在进入主函数前调用 bl copy_data /* 复制 data 段数据到片内内存 data_start */

100ask imx6ull 的 《IMX6ULL裸机开发完全手册》中的 "第13篇 IMX6ULL裸机开发 - 9.4.3 总结:如何在C函数中使用链接脚本变量" 章节讲了如何在 C 程序中调用链接脚本中的表示地址的变量,从而可以在 C 程序中实现 "清零 bss 段"和"数据段搬运到片内内存",而不用在启动代码里完成这些操作。

100ask imx6ull 的 《IMX6ULL裸机开发完全手册》中的 "第13篇 IMX6ULL裸机开发 - 9.5 重定位全部代码" 章节讲了将全部应用的二进制数据搬到芯片的内部内存(128KB),并在其内运行,并且使用 C 程序实现 bss 段清零。其步骤是:第一步,修改链接脚本,段顶位置加上 . = 0x900000;,并加上头、尾的地址标识字符;第二步,在 C 程序中利用头、尾的地址标识字符将其间的数据搬运到芯片内部内存地址;第三步,修改启动文件汇编程序,跳转到内部内存的应用数据处执行。

修改应用在内存中的存放地址

IVT 中的 entry(指示 app.bin 在内存中的位置,即程序数据被复制到内存哪里)的地址在 Makefile 中调用 mkimage 工具时是可以指定,需要改相关联的几个地方如下:

假设应用的二进制数据(app.bin)原来是要存放在内存的 0x80100000 位置,现在要改为 app_address 处。

  1. Makefile 文件中修改 -e 选项后的地址 mkimage -n ./tools/imximage.cfg.cfgtmp -T imximage -e 0x80100000 -d relocate.bin relocate.imx

  2. 链接脚本 imx6ull.lds 中 SECTIONS { . = 0x80100000;... 此处改为 app_address 。

  3. 启动文件 start.S 内,要修改栈地址 sp,ldr sp,=0x80200000 此处根据 app_address 与 0x80100000 的偏移相应修改,对于小的裸机程序,可以至少比 app_address 大 0x00100000。

100ASK IMX6ULL Flashing Tool 工具使用

  • 通过 USB 运行裸机程序(不需要烧写,通过u-boot直接在内存中运行):

    板子设到 USB 启动,在 100ask_imx6ull_flashing_tool 工具中的"专业版"界面,打开 .imx 文件,直接点运行。

  • 通过 USB 烧写裸机程序:

    板子设到 USB 启动,在 Tool 中的"基础版"界面,若选 EMMC ,则用 .imx 文件,若选 SD ,则用 .img 文件。成功后,断电,切到 EMMC 或 SD 启动模式,再上电。

    或者在 win 上,用 win disk imager 工具,把 .img 文件写到 SD 卡。

  • 基础版界面详情:

按钮 作用
烧写整个系统 "选择设备"为EMMC时,把emmc.img烧到EMMC上; "选择设备"为SD/TF时,把sdcard.img烧到SD/TF卡上; "选择设备"为NAND时,把rootfs.ubi烧到Nand Flash上; 并且会烧写对应的U-Boot,请看下面的"更新Uboot"按钮说明。
更新内核 把zImage上传到根文件系统的/boot目录 (对于Nand,是直接烧到内核分区)
更新设备树 把100ask_imx6ull-14x14.dtb上传到根文件系统的/boot目录 (对于Nand,是直接烧到设备树分区)
更新Uboot 对于IMX6ULL全功能版: ①"选择设备"为EMMC时,把u-boot-dtb.imx烧写到EMMC ②"选择设备"为SD/TF时,把u-boot-dtb.imx烧写到SD/TF卡 对于IMX6ULL mini nand版: ①"选择设备"为NAND时,把u-boot-dtb_nand.imx烧写到Nand Flash ②"选择设备"为SD/TF时,把u-boot-dtb_nandsd.imx烧写到SD/TF卡
烧写裸机 把所选裸机文件,烧写到EMMC、SD/TF卡或Nand Flash
上传文件 把所选用户文件,上传到根文件系统的/目录 对于imx6ull mini nand版,无法上传文件(只支持ext4文件系统,而它不是)
相关推荐
西岸行者4 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
我在人间贩卖青春4 天前
汇编之伪指令
汇编·伪指令
悠哉悠哉愿意4 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码4 天前
嵌入式学习路线
学习
毛小茛4 天前
计算机系统概论——校验码
学习
babe小鑫4 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms4 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下4 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。4 天前
2026.2.25监控学习
学习
im_AMBER4 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode