uboot环境中运行HELLOWORLD
源起
手上这块7621板子,万一UBOOT代码有BUG,启不来了,因为没有烧写器在手上,或者有但拆FLASH困难,都只能罢工了。
所以,为了调试UBOOT,最好有一种方法能够在不改FLASH中UBOOT的代码的前提下,运行加载到内存中的UBOOT程序。
既可以直接运行UBOOT中的函数,又可以基于UBOOT的HELLOWORLD,由它来进一步进行复杂一点的函数调用。
UBOOT和HELLOWORLD程序还是物理上以两个BIN存在。这样,只需改动很小的HELLOWORLD,就可以实验。
为什么不仅仅是HELLOWORLD程序呢?
因为那没意义,光HELLOWORLD程序运行起来,只能说明二进制确实是做对了。还不够。
但HELLOWORLD和UBOOT都能跑起来,HELLOWORLD能够随意调用UBOOT的接口,那就强大了,这才相当于有了一个完整的操作系统,HELLOWORLD相当于在这个完整的操作系统上运行了,有GLIBC可以用了,有MEMCPY操作内存了,还有现成的BOOTM函数可以调用了。
这样一来,那不就可以验证自己版本的BOOTM函数了吗?觉得调用起LINUX的过程有疑问或者想跳过某些检查,那就用HELLOWORLD来调用新的DO_BOOTM_LINUX函数试试,这岂不是大大提高了调试的效率吗?
HELLOWORLD程序来源与编译
UBOOT会自带有一个EXAMPLE STANDALONE目录,里面就有hello_world.c源码。
它使用了UBOOT的PRINTF来实现打印。
还调用了UBOOT代码中的get_version()接口。
可见,这个UBOOT的HELLOWROLD满足前面的需求。能调用了PRINTF或者get_version,那,do_bootm_linux不是一样可以调吗?
这是UBOOT系统自带的程序,因此,编译UBOOT时,会在example/standalone/hello_world.bin同时生成这个文件。
通过研究分析Makefile可以看出,这个BIN文件是这样生成的:
首先,.c变.o , 经过gcc -c 是必然的。也很好理解。
其次,.o要用ld来链接,变成ELF。平时,我们在LINUX下编译一个应用程序,是不会用LD的,直接gcc -o 就OK了。但UBOOT下写应用程序不一样,这里为了变成ELF,不能用GCC ,要用LD,要指定基地址-Ttext=0x80200000 ,而且还要指定lds文件,-T example/standalone/mips.lds, 这个mips.lds文件干什么用的呀?a linker script,描述了链接时的参数。由于它的重要性,这里进一步展开讲述:
1 /* SPDX-License-Identifier: GPL-2.0+ */
2 /*
3 * (C) Copyright 2003
4 * Wolfgang Denk Engineering, <wd@denx.de>
5 */
6
7 /*
8 OUTPUT_FORMAT("elf32-bigmips", "elf32-bigmips", "elf32-bigmips")
9 */
10 OUTPUT_FORMAT("elf32-tradbigmips", "elf32-tradbigmips", "elf32-tradlittlemips")
11 OUTPUT_ARCH(mips)
12 SECTIONS
13 {
14 .text :
15 {
16 *(.text*)
17 }
18
19 . = ALIGN(4);
20 .rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
21
22 . = ALIGN(4);
23 .data : { *(.data*) }
24
25 . = .;
26 _gp = ALIGN(16) + 0x7ff0;
27
28 .got : {
29 __got_start = .;
30 *(.got)
31 __got_end = .;
32 }
33
34 .sdata : { *(.sdata*) }
35
36 . = ALIGN(4);
37 __bss_start = .;
38 .sbss (NOLOAD) : { *(.sbss*) }
39 .bss (NOLOAD) : { *(.bss*) . = ALIGN(4); }
40
41 _end = .;
42 }
解析如下:
10行:OUTPUT_FORMAT是什么意思呢?
GNU的文档中,是这么定义的:
OUTPUT_FORMAT(default, big, little),在链接的时候,如果使用了-EB的命令行参数,则使用这里的big参数指定的字节序,如果使用了-EL的命令行参数,则使用这里的little参数指定的字节序,如果没有使用任何命令行参数,则使用这里的default参数指定的字节序。
由mips.lds中的定义OUTPUT_FORMAT(“elf32-tradbigmips”, “elf32-tradbigmips”, “elf32-tradlittlemips”)可见,LD链接时,缺省采用的是elf32-tradbigmips大端字节序。
11行:这是目标体系,7621是mips体系,与常见的另一种以小端字节序的mipsel相对,要注意区分。
12行:SECTIONS 是什么。这就是LDS中的各段描述。
它的完整定义是:
SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
{ contents } >region :phdr =fill
...
}
secname和contents是必须的,其他的都是可选的。
start参数是指定在哪个地址运行
AT指定在哪个地址加载
如果省略AT,那加载地址等于运行地址。
比如:
SECTIONS {
firtst 0x00000000 : { head.o init.o }
second 0x30000000 : AT(4096) { main.o }
}
head.o和init.o按顺序放到0x00000000的加载地址,运行也是这个地址。
main.o放在以0X30000000偏移4096字节处
现在开始SECTIONS,编译之后的BIN文件里,顺序存放的内容分别是(可以打开二进制hello_world.bin对照十六进制内容)
1、14-18行解析
这是一个标准代码段
(.text) ;表示所有的代码部分
2、19行解析
19 . = ALIGN(4);
这是里指定一个.地址,表示当前地址,为当前地址赋值一个4字节对齐的值,再开始下一SEC部分内容。
3、20行 这也是一个标准.rodata 只读数据段
4、22行,. = ALIGN(4); 修改当前地址为4字节对齐后的地址
5、23行,这也是一个标准.data 数据读写段。
6、25行,当前地址更新,不对齐 . = .;
7、26行,_gp = ALIGN(16) + 0x7ff0; 这是一个赋值语句,把当前地址按16字节对齐之后,加上偏移值7FF0后,赋给名为_gp的符号,这个符号在代码中可引用,是等价于printf这样的一个有效符号。
8、紧接着放got段,这是UBOOT特有的非标准段。
9、标准.sdata段
10、36行,4字节对齐
11、37行,在经过4字节对齐之后,这里定义了一个标准的__bss_start符号,等于对齐后的地址。
在hello_world.c中一开始就会找到这个地址,进行清零。
注意__bss_start的地址是这里定义的,不是uboot/System.map 总System.map中定义的__bss_start。
12、.sbss标准段,NOLOAD含义是该段在程序运行时,不被载入内存。但是空间的地址都是保留了的。hello_world运行时,有相应的内存清零工作,完成它的初始化
13、指定.bss标准段。
14、把_end赋值为当前位置,即bss段的结束位置
LD执行实质:
cat .hello_world.cmd
cmd_examples/standalone/hello_world := mipsel-openwrt-linux-musl-ld.bfd -EL -m elf32ltsmip -G 0 -static -n -nostdlib -g -Ttext 0x80200000 -T ./examples/standalone/mips.lds -o examples/standalone/hello_world -e hello_world examples/standalone/hello_world.o examples/standalone/libstubs.o arch/mips/lib/lib.a
看到这里把lib.a链了进来吧,这使得printf的调用成为可能。
这时输出hello_world
用objdump观察
$ mipsel-openwrt-linux-objdump -d hello_world
hello_world: file format elf32-tradlittlemips
Disassembly of section .text:
80200000 <hello_world>:
80200000: 27bdffd8 addiu sp,sp,-40
80200004: afb3001c sw s3,28(sp)
80200008: 00809821 move s3,a0
8020000c: 00a02021 move a0,a1
80200010: afbf0024 sw ra,36(sp)
80200014: afb40020 sw s4,32(sp)
80200018: 3c148020 lui s4,0x8020
8020001c: afb20018 sw s2,24(sp)
80200020: 3c128020 lui s2,0x8020
80200024: afb10014 sw s1,20(sp)
80200028: 00008821 move s1,zero
8020002c: afb00010 sw s0,16(sp)
80200030: 0c0800c6 jal 80200318 <app_startup>
80200034: 00a08021 move s0,a1
80200038: 0c080040 jal 80200100 <dummy>
8020003c: 26520398 addiu s2,s2,920
80200040: 3c048020 lui a0,0x8020
80200044: 00402821 move a1,v0
80200048: 0c080054 jal 80200150 <printf>
为了在UBOOT上能够运行,做成ELF还不够,这个ELF文件是不能直接运行的,要用OBJCOPY剥掉这个ELF头,才是最终UBOOT上运行的代码hello_world.bin
其执行实质是:
cat .hello_world.bin.cmd
cmd_examples/standalone/hello_world.bin := mipsel-openwrt-linux-musl-objcopy -O binary examples/standalone/hello_world examples/standalone/hello_world.bin
hello_world.bin的加载运行
两步:
1、先加载同时编译出来的uboot.bin,为什么啊?这相当于要给HELLOWROLD准备好PRINTF等函数所在的库啊。
2、再加载hello_world.bin,而且是必须加载到相同的首地址。为什么啊?这是准备要通过go指令去执行的程序啊。
go指令跳到hello_world.bin所加载的首地址之后,里面的代码会进一步引用了uboot.bin的地址。
在hello_world.bin生成的时候,是指定过基地址的,才能将printf的符号转变成以基地址为基准的绝对地址。
在运行时,如果这个要找的PRINTF绝对地址处没有PRINTF的代码,那当然就不能执行了呀。
所以才有把UBOOT.BIN先加载到同一个基地址的要求。
这种往基地址写两份代码的做法,存在一个缺陷,就是重合部分的UBOOT代码功能丢失了。
好在HELLOWORLD很小,这部分代码不多,也就影响不大。
执行过程:
Hit any key to stop autoboot: 0
*** U-Boot Boot Menu ***
1. Startup system (Default)
2. Upgrade firmware
3. Upgrade Factory
4. Upgrade bootloader
5. Upgrade bootloader (advanced mode)
6. Load image
0. U-Boot console
Press UP/DOWN to move, ENTER to select
=> tftp 80200000 boot.bin
Using eth@1e100000 device
TFTP from server 192.168.100.191; our IP address is 192.168.100.3
Filename 'boot.bin'.
Load address: 0x80200000
Loading: ###################
6.6 MiB/s
done
Bytes transferred = 269960 (41e88 hex)
=> tftp 80200000 hello_world.bin
Using eth@1e100000 device
TFTP from server 192.168.100.191; our IP address is 192.168.100.3
Filename 'hello_world.bin'.
Load address: 0x80200000
Loading: #
507.8 KiB/s
done
Bytes transferred = 1040 (410 hex)
=> md 80200000
80200000: 27bdffd8 afb3001c 00809821 00a02021 ...'....!...! ..
80200010: afbf0024 afb40020 3c148020 afb20018 $... ... ..<....
80200020: 3c128020 afb10014 00008821 afb00010 ..<....!.......
80200030: 0c0800c6 00a08021 3c048020 24050009 ....!... ..<...$
80200040: 2484034c 0c080054 265203a8 0c080040 L..$T.....R&@...
80200050: 00000000 3c048020 00402821 0c080054 .... ..<!(@.T...
80200060: 2484036c 3c048020 0c080054 2484038c l..$ ..<T......$
80200070: 3c048020 02602821 0c080054 2484039c ..<!(`.T......$
80200080: 0271102a 1440000a 3c048020 8e060000 *.q...@. ..<....
80200090: 50c00001 26860344 02202821 0c080054 ...PD..&!( .T...
802000a0: 02402021 26310001 1000fff5 26100004 ! @...1&.......&
802000b0: 0c080054 248403bc 0c080048 00000000 T......$H.......
802000c0: 1040fffd 00000000 0c080044 00000000 ..@.....D.......
802000d0: 3c048020 0c080054 248403d8 00001021 ..<T......$!...
802000e0: 8fbf0024 8fb40020 8fb3001c 8fb20018 $... ...........
802000f0: 8fb10014 8fb00010 03e00008 27bd0028 ............(..'
=> go 80200000
## Starting application at 0x80200000 ...
Example expects ABI version 9
Actual U-Boot ABI version 9
Hello World
argc = 1
argv[0] = "80200000"
argv[1] = "<NULL>"
Hit any key to exit ...
## Application terminated, rc = 0x0
=>
常见问题
1、运行hello_world时异常Ooops
=> tftp 80100000 boot.bin
Using eth@1e100000 device
TFTP from server 192.168.100.191; our IP address is 192.168.100.3
Filename 'boot.bin'.
Load address: 0x80100000
Loading: ###################
6.6 MiB/s
done
Bytes transferred = 269960 (41e88 hex)
=> tftp 80100000 hello_world.bin
Using eth@1e100000 device
TFTP from server 192.168.100.191; our IP address is 192.168.100.3
Filename 'hello_world.bin'.
Load address: 0x80100000
Loading: #
507.8 KiB/s
done
Bytes transferred = 1040 (410 hex)
=> md 80100000
80100000: 27bdffd8 afb3001c 00809821 00a02021 ...'....!...! ..
80100010: afbf0024 afb40020 3c148020 afb20018 $... ... ..<....
80100020: 3c128020 afb10014 00008821 afb00010 ..<....!.......
80100030: 0c0800c6 00a08021 3c048020 24050009 ....!... ..<...$
80100040: 2484034c 0c080054 265203a8 0c080040 L..$T.....R&@...
80100050: 00000000 3c048020 00402821 0c080054 .... ..<!(@.T...
80100060: 2484036c 3c048020 0c080054 2484038c l..$ ..<T......$
80100070: 3c048020 02602821 0c080054 2484039c ..<!(`.T......$
80100080: 0271102a 1440000a 3c048020 8e060000 *.q...@. ..<....
80100090: 50c00001 26860344 02202821 0c080054 ...PD..&!( .T...
801000a0: 02402021 26310001 1000fff5 26100004 ! @...1&.......&
801000b0: 0c080054 248403bc 0c080048 00000000 T......$H.......
801000c0: 1040fffd 00000000 0c080044 00000000 ..@.....D.......
801000d0: 3c048020 0c080054 248403d8 00001021 ..<T......$!...
801000e0: 8fbf0024 8fb40020 8fb3001c 8fb20018 $... ...........
801000f0: 8fb10014 8fb00010 03e00008 27bd0028 ............(..'
=> go 80100000
## Starting application at 0x80100000 ...
Ooops:
$ 0 : 00000000 00000000 0000002a be000c00
$ 4 : 00000001 8fe7ef0c 8fe7ef0c 00000001
$ 8 : 00000000 00000004 8ffe1bd8 0000000f
$12 : 8fe7ebf8 00000004 00000002 00000002
$16 : 00000002 8fe7ef08 80100000 8fe7ef08
$20 : 8fe7ef4c 00000002 00000020 00001000
$24 : 00000200 80100000
$28 : be10a818 8fe7eca0 80007b40 8ffa4a60
Hi : 00000000
Lo : 00000004
epc : 80100000 (text 70360000)
ra : 8ffa4a60 (text 80204a60)
Status: 00000006
Cause : e000802c (ExcCode 0b)
PrId : 0001992f
### ERROR ### Please RESET the board ###
这个原因是go跳转地址是80100000,不等于helloworld的链接地址80200000,所以无法执行。
2、运行hello_world时异常也是Ooops
虽然 地址对,但uboot.bin没有TFTP到指定地址,也会出现OOP,因为要调用的函数不存在。此时OOP的EPC会指向要调用的函数的地址。