Skip to content

LV065-代码怎么运行的

我们分析的时候,以韦东山教程中的文件为例进行分析,我们前边的 Makefile 文件中已经可以生成反汇编文件,我们可以直接查看。

一、文件准备

1. start.s

assembly
.text
.global  _start
_start: 				
	ldr  sp,=0x80200000
	bl clean_bss
	bl main

halt:
	b  halt 

clean_bss:
	/* 清除BSS段 */
	ldr r1, =__bss_start
	ldr r2, =__bss_end
	mov r3, #0
clean:
	str r3, [r1]
	add r1, r1, #4
	cmp r1, r2
	bne clean
	
	mov pc, lr

2. 链接文件

assembly
SECTIONS {
    . = 0x80100000;

    . = ALIGN(4);
    .text      :
    {
      *(.text)
    }

    . = ALIGN(4);
    .rodata : { *(.rodata) }

    . = ALIGN(4);
    .data : { *(.data) }

    . = ALIGN(4);
    __bss_start = .;
    .bss : { *(.bss) *(.COMMON) }
    __bss_end = .;
}

3. C 程序文件

3.1 led.c

c
#include "led.h"

static volatile unsigned int *CCM_CCGR1                              ;
static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
static volatile unsigned int *GPIO5_GDIR                             ;
static volatile unsigned int *GPIO5_DR                               ;

void led_init(void)
{
	unsigned int val;
	
	CCM_CCGR1                               = (volatile unsigned int *)(0x20C406C);
	IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = (volatile unsigned int *)(0x2290014);
	GPIO5_GDIR                              = (volatile unsigned int *)(0x020AC000 + 0x4);
	GPIO5_DR                                = (volatile unsigned int *)(0x020AC000);

	/* GPIO5_IO03 */
	/* a. 使能 GPIO5
	 * set CCM to enable GPIO5
	 * CCM_CCGR1 [CG15] 0x20C406C
	 * bit [31:30] = 0b11
	 */
	*CCM_CCGR1 |= (3<<30);
	
	/* b. 设置 GPIO5_IO03 用于 GPIO
	 * set IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3
	 *      to configure GPIO5_IO03 as GPIO
	 * IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3  0x2290014
	 * bit [3:0] = 0b0101 alt5
	 */
	val = *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
	val &= ~(0xf);
	val |= (5);
	*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = val;
	
	
	/* c. 设置 GPIO5_IO03 作为 output 引脚
	 * set GPIO5_GDIR to configure GPIO5_IO03 as output
	 * GPIO5_GDIR  0x020AC000 + 0x4
	 * bit [3] = 0b1
	 */
	*GPIO5_GDIR |= (1<<3);

}

void led_ctl(int on)
{
	if (on) /* on: output 0*/
	{
		/* d. 设置 GPIO5_DR 输出低电平
		 * set GPIO5_DR to configure GPIO5_IO03 output 0
		 * GPIO5_DR 0x020AC000 + 0
		 * bit [3] = 0b0
		 */
		*GPIO5_DR &= ~(1<<3);
	}
	else  /* off: output 1*/
	{
		/* e. 设置 GPIO5_IO3 输出高电平
		 * set GPIO5_DR to configure GPIO5_IO03 output 1
		 * GPIO5_DR 0x020AC000 + 0
		 * bit [3] = 0b1
		 */ 
		*GPIO5_DR |= (1<<3);
	}
}

3.2 led.h

c
#ifndef   __LED_H__
#define   __LED_H__

void led_init(void);
void led_ctl(int on);

#endif

3.3 main.c

c
#include "led.h"

void delay(volatile unsigned int d)
{
	while(d--);
}


int  main()
{
	led_init();

	while(1)
	{
		led_ctl(1);
		delay(1000000);
		led_ctl(0);
		delay(1000000);
	}
					
	return 0;
}

4. 工具准备

还有一个生成 DCD 数据的工具,需要去 u-boot 的源码中拷贝,所需文件如下:

image-20230723091404100

5. 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
	$(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 
	
	$(OBJCOPY) -O binary -S led.elf  led.bin
	$(OBJDUMP) -D -m arm  led.elf  > led.dis	
	./tools/mkimage -n ./tools/imximage.cfg.cfgtmp -T imximage -e 0x80100000 -d led.bin led.imx
	dd if=/dev/zero of=1k.bin bs=1024 count=1
	cat 1k.bin led.imx > led.img

clean:
	rm -f led.dis  led.bin led.elf led.imx led.img *.o

二、上电后在做啥?

如下图, imx6ull 芯片一上电后,会先执行 bootRom 程序,此程序是芯片出厂时已经固定的程序,除了芯片原厂,我们是无法修改的。

image-20230722195231658

(1)bootRom 会把 EMMC 或 TF 卡的前 4K 数据读入到芯片内部 RAM 运行。

(2)bootRom 根据 DCD 进行初始化 DDR。

(3)bootRom 根据 IVT,从 EMMC 或 TF 卡中将 led.bin 读到 DDR 的 0x80100000 地址

(4)跳转到 DDR 的 0x80100000 地址执行 。

目前 led.bin 程序已经复制到内存中, CPU 开始从内存 0x80100000 地址开始执行机器码,每一条机器码是 32 位/4 字节,此处的机器码就是 led.bin 中的 机器码,我们可以打开 led.bin 看一下机器码是不是上边图中画的:

image-20230722195704437

前面介绍过大/小端模式,从这里就可以看出来啦。此处可以看到机器码 e59fd028(指令: ldr sp,= 0x80200000)的存储形式:

c
   地址   机器码  
00000000   28
00000001   d0
00000002   9f
00000003   e5

可以看到 imx6ull 的存储方式是小端模式,其实, ARM 处理器中存储方式一般都是 小端模式

三、运行过程分析

这一部分主要是分析 led.dis 文件。

1. start.s 的反汇编

image-20230722201137948

(1)CPU 执行的第一条机器码就是内存地址 0x80100000 存储的 e59fd028 机器码对应的指令是“ ldr sp, [pc, #40] ”,相当于 start.s 文件的 “ ldr sp,= 0x80200000 ” 指令。执行完后,寄存器 SP 的值等于 0x80200000。

assembly
80100000: e59fd028 ldr sp, [pc, #40] ; 80100030 <clean+0x14>

(2)每执行完一条机器码,会自动执行下一个内存地址 0x80100004 存储的 eb000001 机器码对应的指令是“ bl 80100010”,相当于 Start.S 文件的“ bl clean_bss”指令。

assembly
80100004:	eb000001 	bl	80100010 <clean_bss>

@......

80100010 <clean_bss>:
80100010:	e59f101c 	ldr	r1, [pc, #28]	; 80100034 <clean+0x18>
80100014:	e59f201c 	ldr	r2, [pc, #28]	; 80100038 <clean+0x1c>
80100018:	e3a03000 	mov	r3, #0

(3)跳转到内存地址 0x80100010 执行 e59f101c 机器码,对应的指令是 ldr r1, [pc, #28] 。相当于 start.s 文件的“ ldr r1, =__bss_start”指令。

(4)此处 clean_bss 相当于一个函数, CPU 会逐条执行指令,clean 为循环体,直到执行“ mov pc, lr”指令后,才返回。

(5)最后返回哪里?返回内存地址 0x80100008 处执行 fa000057 机器码,对应的指令是“blx 8010016c”。对应 start.s 文件的“ bl main”指令 。

2. 进入 main 函数

我们简单看一下 main 函数的反汇编文件对应情况:

image-20230722202506845

(1)进入 main()函数后,先将寄存器 R7、 LR 入栈,保存现场/上下文,方便 main()函数执行完毕后返回,并且将当前栈指向的内存地址赋值给寄存器 R7。

(2)调用 led_init()函数,因为没有参数传递,所以直接调用 BL 指令进行跳转,即“bl 8010003c”指令。

(3)调用 led_ctl(1)函数,此处只有一个参数,通过寄存器 R0 进行传递,即“movs r0, #1”指令,然后通过 BL 指令进行跳转,即“bl 801000f8”指令,关于参数传递问题,可以参考前面《10-开发平台/15-ARM 汇编》的笔记。

(4)调用 delay(1000000)函数,此处只有一个参数,通过寄存器 R0 进行传递,然后通过 BL 指令进行跳转。

(5)调用 led_ctl(0)函数,此处只有一个参数,通过寄存器 R0 进行传递,然后通过 BL 指令进行跳转。

(6)调用 delay(1000000)函数,此处只有一个参数,通过寄存器 R0 进行传递,然后通过 BL 指令进行跳转。

(7)while(1)循环体到此已经结束,但是需要循环执行循环体的内容,通过 B 指令进行跳转到循环体开头,即“b.n 80100174”指令,执行内存地址 0x80100174 处的指令,也就是 led_ctl(1)函数对应的汇编指令“movs r0,#1”。