获取当前芯片平台的相关信息
为了深入了解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;
注意到,relocaddr
与reloc_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个字节;此外,笔者的自拷贝是在关闭了dcache
和icache
的条件下执行的(由新增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_RELATIVE
在u-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_offset
为0x80028
,增量数据r_addend
为0x843b0
;结合此处(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_addend
与r_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
中实现的这一重定位操作复杂,以后再探究。