获取当前芯片平台的相关信息

为了深入了解ARM 64位芯片架构,笔者为u-boot添加了archinfo命令,以获取CPU当前的工作状态等信息。不过在增加完整的64位ARM架构信息查看功能之前,笔者首先增加了简单的获取当前PC指针及栈指针的函数:

diff --git a/arch/arm/lib/arch-info.c b/arch/arm/lib/arch-info.c
new file mode 100644
index 00000000..b9bc9fd0
--- /dev/null
+++ b/arch/arm/lib/arch-info.c
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+
+#include <archinfo.h>
+#include <asm/global_data.h>
+
+DECLARE_GLOBAL_DATA_PTR;
+
+ulong archinfo_getpc(ulong * stkp)
+{
+	ulong rval;
+
+	rval = (ulong) __builtin_return_address(0);
+	if (stkp != NULL) {
+		ulong stkval = 0;
+		asm volatile ("\tmov %0, sp\n" : "=r"(stkval));
+		*stkp = stkval;
+	}
+
+	return rval;
+}
diff --git a/cmd/archinfo.c b/cmd/archinfo.c
new file mode 100644
index 00000000..5a474ce4
--- /dev/null
+++ b/cmd/archinfo.c
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+#include <archinfo.h>
+#include <command.h>
+#include <asm/global_data.h>
+
+DECLARE_GLOBAL_DATA_PTR;
+
+static int do_archinfo(struct cmd_tbl *cmdtp,
+	int flag, int argc, char *const argv[])
+{
+	ulong pcval, stack = 0;
+
+	pcval = archinfo_getpc(&stack);
+	printf("Current PC: 0x%lx, stack: 0x%lx\n",
+		pcval, stack);
+
+	return 0;
+}
+
+/**************************************************/
+U_BOOT_CMD(
+	archinfo, 3, 1, do_archinfo,
+	"display architectural dependent information",
+	"archinfo"
+);

针对笔者调试使用到的设备,u-boot镜像的基地址CONFIG_SYS_TEXT_BASE定义为0x80000;因镜像文件比较小,获得的PC指针应当与当前的基地址相去不远:

Disassembly of section .text:

0000000000080000 <__image_copy_start>:
   80000:   1400000c    b   80030 <reset> 
   80004:   d503201f    nop

0000000000080008 <_TEXT_BASE>:
   80008:   00080000    .word   0x00080000
   8000c:   00000000    .word   0x00000000

0000000000080010 <_end_ofs>:
   80010:   000850b0    .word   0x000850b0
   80014:   00000000    .word   0x00000000

上面新增代码需要修改相关Makefile才能编译链接成功。加载新的u-boot镜像后,执行archinfo命令:

Hit any key to stop autoboot:  0 
U-Boot> archinfo
Current PC: 0x3b36492c, stack: 0x3af5bb10
U-Boot> 

这个结果与期望结果相去甚远。暂且不关注栈指针的值,通过对u-boot的反汇编可知,当前的PC指针应当为0x8492c

0000000000084918 <do_archinfo>:
   84918:   a9be7bfd    stp x29, x30, [sp, #-32]!
   8491c:   910003fd    mov x29, sp 
   84920:   910083a0    add x0, x29, #0x20
   84924:   f81f8c1f    str xzr, [x0, #-8]!
   84928:   97fff967    bl  82ec4 <archinfo_getpc>
   8492c:   aa0003e1    mov x1, x0

二者相差了0x3b36492c - 0x8492c = 0x3b2e0000;这个偏量是如何产生的?目前可以肯定u-boot在启动过程中给自身进行了重定位;不过根据笔者之前的一篇文章(仅是u-boot的自拷贝),这个重定位到任意地址并执行(Position Independent Execute)对于基地址固定的u-boot镜像文件来说,是不可能的。笔者以过去的经验犯了一个错误,这个调试结果刷新了笔者的认知。不过,这并不意味着之前的工作白费了:之前已测试,u-boot必须在加载到基地址处启动才不会有异常。接下来就需要分析u-boot的重定位功能了,这是一个探究的过程。

u-boot的板级信息查看

u-boot提供了bdinfo命令以查看设备相关的板级信息。在笔者的调试设备上,板级信息中恰好存在上面计算得到的偏移量,从而可以从板级信息入手,定位u-boot重定位的地址如何确定的:

U-Boot> bdinfo
boot_params = 0x0000000000000100
DRAM bank   = 0x0000000000000000
-> start    = 0x0000000000000000
-> size     = 0x000000003b400000
flashstart  = 0x0000000000000000
flashsize   = 0x0000000000000000
flashoffset = 0x0000000000000000
baudrate    = 115200 bps
relocaddr   = 0x000000003b360000
reloc off   = 0x000000003b2e0000

经一番查找,确定了与u-boot重定位地址相关的计算的代码如下:

/* common/board_f.c */
 343     gd->ram_top = gd->ram_base + get_effective_memsize();
 344     gd->ram_top = board_get_usable_ram_top(gd->mon_len);
 345     gd->relocaddr = gd->ram_top;
 ...
 679 #ifdef CONFIG_SYS_TEXT_BASE
 680 #ifdef ARM
 681     gd->reloc_off = gd->relocaddr - (unsigned long)__image_copy_start;

注意到,relocaddrreloc_off之差恰好为u-boot镜像加载的基地址,也是符号__image_copy_start所在的地址。可见,u-boot在启动过程中,将自身重定位到了可用内存的顶部ram_top附近的内存区域。这样做的原因是什么?我想可能是为了方便u-boot对内存的管理,并可以将新的u-boot加载到预定义的基地址。不过,为设备配置一个板级参数GD_FLG_SKIP_RELOC可以跳过多个u-boot资源的重定位。

u-boot的编译选项

笔者一直认为由-fPIC-fPIE参数编译生成的目标文件,链接成动态库或可执行文件后,在运行时可动态地重定位,从而实现了“位置无关地执行”,即Position Independent Execute。不过笔者犯了经验主义的错误,查看u-boot的编译命令可以肯定,u-boot编译的选项同时包含-fno-pic-fno-PIE两个参数:

  aarch64-linux-gnu-gcc -Wp,-MD,cmd/.gpio.o.d  -nostdinc -isystem /opt/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin/../lib/gcc/aarch64-linux-gnu/7.5.0/include -Iinclude   -I./arch/arm/include -include ./include/linux/kconfig.h -D__KERNEL__ -D__UBOOT__ -Wall -Wstrict-prototypes -Wno-format-security -fno-builtin -ffreestanding -std=gnu11 -fshort-wchar -fno-strict-aliasing -fno-PIE -Os -fno-stack-protector -fno-delete-null-pointer-checks -Wno-maybe-uninitialized -g -fstack-usage -Wno-format-nonliteral -Wno-unused-but-set-variable -Werror=date-time -D__ARM__ -fno-pic -mstrict-align -ffunction-sections -fdata-sections -fno-common -ffixed-r9 -fno-common -ffixed-x18 -pipe -march=armv8-a -D__LINUX_ARM_ARCH__=8 -I./arch/arm/mach-bcm283x/include    -DKBUILD_BASENAME='"gpio"'  -DKBUILD_MODNAME='"gpio"' -c -o cmd/gpio.o cmd/gpio.c

u-boot虽然启用了mmu,但通常只是一一映射,虚拟地址等同于物理地址。这两个编译参数可以使得编译的目标文件中没有多余的段,如动态库常见的.got(Global Offset Table)、.got.plt以及.dynamic等数据段,方便链接脚本的编写,以及重定位的实现。

u-boot重定位的实现

为笔者调式设备编译u-boot,在代码根目录下会生成一个链接脚本,其部分内容如下:

SECTIONS
{
 . = 0x00000000;
 . = ALIGN(8);
 .text :
 {
  *(.__image_copy_start)
  arch/arm/cpu/armv8/start.o (.text*)
 }
 .efi_runtime : {
                __efi_runtime_start = .;
  *(.text.efi_runtime*)
  *(.rodata.efi_runtime*)
  *(.data.efi_runtime*)
                __efi_runtime_stop = .;
 }
 .text_rest :
 {
  *(.text*)
 }
 . = ALIGN(8);
 .rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
 . = ALIGN(8);
 .data : {
  *(.data*)
 }
 . = ALIGN(8);
 . = .;
 . = ALIGN(8);
 .u_boot_list : {
  KEEP(*(SORT(.u_boot_list*)));
 }
 . = ALIGN(8);
 .efi_runtime_rel : {
                __efi_runtime_rel_start = .;
  *(.rel*.efi_runtime)
  *(.rel*.efi_runtime.*)
                __efi_runtime_rel_stop = .;
 }
 . = ALIGN(8);
 .image_copy_end :
 {
  *(.__image_copy_end)
 }
 . = ALIGN(8);
 .rel_dyn_start :
 {
  *(.__rel_dyn_start)
 }
 .rela.dyn : {
  *(.rela*)
 }
 .rel_dyn_end :
 {
  *(.__rel_dyn_end)
 }
 _end = .;

链接脚本导出了四个重要的符号,分别为:拷贝代码的起始地址__image_copy_start(实际上为CONFIG_SYS_TEXT_BASE定义的基地址)、拷贝的终止地址__image_copy_end、重定位数据的起始地址__rel_dyn_start和终止地址__rel_dyn_end。这四个符号在重定位代码实现中被引用,首先,拷贝代码段(.text)和数据段(.rodata.data):

/*  arch/arm/lib/relocate_64.S */
    adrp    x1, __image_copy_start      /* x1 <- address bits [31:12] */
    add x1, x1, :lo12:__image_copy_start/* x1 <- address bits [11:00] */
    adrp    x2, __image_copy_end        /* x2 <- address bits [31:12] */
    add x2, x2, :lo12:__image_copy_end  /* x2 <- address bits [11:00] */
copy_loop:
    ldp x10, x11, [x1], #16 /* copy from source address [x1] */
    stp x10, x11, [x0], #16 /* copy to   target address [x0] */
    cmp x1, x2          /* until source end address [x2] */
    b.lo    copy_loop

这个拷贝的汇编实现,比笔者的自拷贝效率高一倍,每次复制16个字节;此外,笔者的自拷贝是在关闭了dcacheicache的条件下执行的(由新增goraw命令关闭),可以推测,复制完成之后会有一个刷数据缓存和指令缓存的操作:

/*  arch/arm/lib/relocate_64.S */
relocate_done:
    switch_el x1, 3f, 2f, 1f
    bl  hang
3:  mrs x0, sctlr_el3
    b   0f
2:  mrs x0, sctlr_el2
    b   0f
1:  mrs x0, sctlr_el1
0:  tbz w0, #2, 5f  /* skip flushing cache if disabled */
    tbz w0, #12, 4f /* skip invalidating i-cache if disabled */
    ic  iallu       /* i-cache invalidate all */
    isb sy
4:  ldp x0, x1, [sp, #16]
    bl  __asm_flush_dcache_range
    bl     __asm_flush_l3_dcache
5:  ldp x29, x30, [sp],#32
    ret

接下来重要的操作是重定位数据的外理了。重定位数据有两种固定的格式,通过man elf可以得到相关信息。对于64位ELF文件,其定义为:

typedef struct {
    Elf64_Addr r_offset;
    uint64_t   r_info;
} Elf64_Rel;

typedef struct {
    Elf64_Addr r_offset;
    uint64_t   r_info;
    int64_t    r_addend;
} Elf64_Rela;

重定位数据的处理只考虑了第二种格式,Elf64_Rela;笔者认为,可能是因为u-boot是静态链接的,镜像文件中不存在第一种格式的重定位数据;也可能是为了对齐,两种格式的重定位数据大小被设定相同。r_offset指定了需要重定位的内存偏移地址,该地址的数据在不重定位的情况下不需要修改(那么可以推测,对于静态链接的可执行文件,没有.rela重定位数据段的情况下,也能够正常运行);r_info指定了重定位的类型,r_addend则用于计算相对偏移重定位的一个偏移。u-boot对重定向数据的处理如下:

    /*  
     * Fix .rela.dyn relocations
     */
    adrp    x2, __rel_dyn_start     /* x2 <- address bits [31:12] */
    add x2, x2, :lo12:__rel_dyn_start   /* x2 <- address bits [11:00] */
    adrp    x3, __rel_dyn_end       /* x3 <- address bits [31:12] */
    add x3, x3, :lo12:__rel_dyn_end /* x3 <- address bits [11:00] */
fixloop:
    ldp x0, x1, [x2], #16   /* (x0,x1) <- (SRC location, fixup) */
    ldr x4, [x2], #8        /* x4 <- addend */
    and x1, x1, #0xffffffff
    cmp x1, #R_AARCH64_RELATIVE
    bne fixnext

    /* relative fix: store addend plus offset at dest location */
    add x0, x0, x9
    add x4, x4, x9
    str x4, [x0]
fixnext:
    cmp x2, x3
    b.lo    fixloop

R_AARCH64_RELATIVEu-boot中被定义为0x403,在elf.h中定义相同:

/* /usr/include/elf.h */
#define R_AARCH64_RELATIVE     1027 /* Adjust by program base.  */

上面的汇编代码表明,u-boot只处理程序代码断的偏移;即修改保存于镜像中的函数地址。它会读取r_addend,加上一个偏移量(由x9寄存量保存),写入到相应的、新的r_offset地址处(原先的r_offset加上偏移量)。通过aarch64-linux-gnu-objdump-D选项反汇编u-boot可得到:

Disassembly of section .rela.dyn:

00000000000f9788 <__image_copy_end>:
   f9788:   00080028    .word   0x00080028    # r_offset
   f978c:   00000000    .word   0x00000000
   f9790:   00000403    .word   0x00000403    # r_info -> R_AARCH64_RELATIVE
   f9794:   00000000    .word   0x00000000
   f9798:   000843b0    .word   0x000843b0    # r_addend
   f979c:   00000000    .word   0x00000000

   f97a0:   000810d0    .word   0x000810d0    # r_offset
   f97a4:   00000000    .word   0x00000000
   f97a8:   00000403    .word   0x00000403    # r_info -> R_AARCH64_RELATIVE
   f97ac:   00000000    .word   0x00000000
   f97b0:   000810d0    .word   0x000810d0    # r_addend
   f97b4:   00000000    .word   0x00000000
   
   f97b8:   000810d8    .word   0x000810d8    # r_offset
   f97bc:   00000000    .word   0x00000000
   f97c0:   00000403    .word   0x00000403    # r_info -> R_AARCH64_RELATIVE
   f97c4:   00000000    .word   0x00000000
   f97c8:   000810d0    .word   0x000810d0    # r_addend
   f97cc:   00000000    .word   0x00000000

上面的空格及注释是笔者加入的,仅列出了三条重定位结构体Elf64_Rela。处理第一个重定位数据,其r_offset0x80028,增量数据r_addend0x843b0;结合此处(0x80028地址处)的数据,可知重定位操作主要是修改了函数的绝对地址:

/* Disassembly of u-boot, u-boot.S */
0000000000080028 <_save_boot_params>:
   80028:   000843b0    .word   0x000843b0
   8002c:   00000000    .word   0x00000000

/* arch/arm/cpu/armv8/start.S */
_save_boot_params:
    .quad save_boot_params

上面的重定位汇编代码中的x9若为零,那么r_offset地址偏移处的数据与r_addend完全相同,修改r_offset前后的数据完全相同,那么重定位出就不需要了。这也就证明了上面的推断:若不重定位,u-boot镜像中不需要.rela段数据;静态链接的u-boot自身已是完整的了,但只能在基地址处执行。最后,第二和第三个重定位数据的r_addendr_offset偏移地址处的数据也相同,更加坚定了这种解读的正确性:

/* Disassembly of u-boot, u-boot.S */
00000000000810d0 <efi_runtime_mmio>:
   810d0:   000810d0    .word   0x000810d0
   810d4:   00000000    .word   0x00000000
   810d8:   000810d0    .word   0x000810d0
   810dc:   00000000    .word   0x00000000
   
/* lib/efi_loader/efi_runtime.c */
LIST_HEAD(efi_runtime_mmio);       /* -> 指向自身的空链表(双链) */

本次调试、分析,刷新了笔者的认知,同时也加深了对u-boot的理解;以后再也不能肯定地说:u-boot只能运行在基地址偏移处,u-boot的行为比之前所知的要复杂得多。运行于Linux系统下的应用,在加载动态链接库时,代码和数据的重定位操作远比u-boot中实现的这一重定位操作复杂,以后再探究。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐