上一篇文章,讲述了制作U-Boot
烧写镜像到SD卡的过程,其中运用make
的方式来进行将.s
文件编译成.bin
文件,那make
是什么意思?它主要实现了什么?
先讲一下,如果不采用make
的方式该怎样实现这个过程。
先准备两个.s
文件,myboot.s
和mylowlevel_init.s
。为了使用讲解一下链接
过程,本文故意将gpio_out
和led2_on
两个过程写在两个文件中。
myboot.s:
b reset //8种异常的处理,都是跳转到reset
b reset
b reset
b reset
b reset
b reset
b reset
reset:
bl gpio_out
bl led2_on
mov r0, r1 //5句无用代码
mov r1, r2
mov r2, r3
mov r3, r4
mov r4, r5
1: //标号
b 1b //死循环
gpio_out:
ldr r11, =0xE0200280 //获得寄存器地址
ldr r12, =0x00001111 //配置成输出状态
str r12, [r11] //将r12寄存器的值放回r11
ldr r11, =0xE0200284
ldr r12, =0xF
str r12, [r11]
mov pc, lr
mylowlevel_init.s:
.globl led2_on
led1_on:
ldr r11, =0xE0200284
ldr r12, [r11]
bic r12, r12, #(1<<1) //将r12的第二位清零,回写到r12
str r12, [r11]
mov pc, lr
下面就先将.s
文件汇编成.o
文件,利用arm-linux-gcc
命令。
arm-linux-gcc -c mystart.s
arm-linux-gcc -c mylowlevel_init.s
那么两个.o
文件怎么链接
成一个可执行文件呢?
当对一个.c
文件进行处理,经过预编译
、编译
、汇编
后,最终会生成.o
文件。最后一步,将所有的.o
文件进行链接
,从而生成最终的可执行文件。
那么如何进行链接呢?
汇编后的.o
文件会由好多段(section
)组成,其中最基本的段就是:bss段
,data段
和text段
。这些段的含义如下:
bss段
:bss段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域,属于静态内存分配;data段
:数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域,属于静态内存分配;text段
:代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读(某些架构也允许代码段为可写,即允许修改程序)。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。在链接的过程中,当然不会喜欢将这些段直接拼接,而是希望它们按种类进行分别拼接,也就是说:
对于标准C程序而言,链接是固定的。它是ld
调用一个缺省的链接脚本来完成的。因此对于一般的应用开发者,几乎感觉不到ld
以及链接脚本的存在。
但是如果在一些特殊情况下,主要是底层非操作系统程序。里面很多代码,特别是汇编代码。必须要链接到指定的位置。而且这个时候的程序入口不一定也不是main了。这种情况在bootloader、Linux内核以及裸机程序下比较普遍,这时就要手工编写lds
文件了。
总而言之,创建.o
可执行文件的最后一步就是链接。它是由ld
或者是用gcc
间接调用ld
来完成的。它主要任务和把外部库和应用程序目标代码的各个段放到正确位置。
那么,本文就先创建一个简单的LDS
文件(myboot.lds
)来进行链接:
SECTIONS
{
. = 0xD0020010 //链接完的可执行文件开始运行的地址(即定位器位置)
.text : {
mystart.o //mystart.o的.text最前面
* (.text) //剩余文件的.text都放在后面,先后顺序不管
}
.data : {
* (.data) //所有文件的.data放在一起
}
.bss_start = .; //.bss_start在当前位置
.bss : { //可能在其他的文件中用到
* (.bss)
}
.bss_end = .; //.bss_end在当前位置
}
想要了解更多的内容,可以参考博文:Linux下的lds链接脚本详解。
链接部分使用arm-linux-ld
指令:
arm-linux-ld -T myboot.lds -o myboot mystart.o mylowlevel_init.o
查看一下mystart.o
文件:
arm-linux-objdump -S mystart.o
运行结果如图所示:
可以看到,由于未链接所以地址从0开始。前面8句都是跳转到地址20,地址20就是reset语句。但是,可以看到所有的汇编代码相同,但是机器指令却是不同的。
根据机器指令,如何计算出汇编代码呢?
根据ARM的手册,可以查看这些机器指令的含义,这里就不展开了。ea000006
就表示向后跳转6条指令;而pc指针等于当前地址+8,即指向后两条指令处。总共是向后跳转8条指令,也就是reset处。这样就可以理解了。
另外,还看到led2_on
的汇编代码,由于还未进行链接不知道跳转到哪里去,所以显示的地址为0。
除此之外,还看到gpio_out
的代码有所变化,r11变成了fp,r12变成了ip,这其实就是ARM内核将r11、r12寄存器增加了新的功能,但是在一般情形下,也可以当做普通寄存器,不需要管。
还有,由于ARM的每条指令为4个字节,但是立即数0xE0200280
直接就已经是4个字节了。因此,ARM就将这一条指令变成了两条指令,一条指令将0xE0200280
放在一个新的地址上,另一条指令通过相对指针偏转指令来进行读取。
40: e59fb014 ldr fp, [pc, #20]
5c: e0200280 .word 0xe0200280
顺便也查看一下mylowlevel_init.o
文件
arm-linux-objdump -S mylowlevel_init.o
运行结果如图所示:
就不再进行分析了。
最后看一下u-boot
可执行文件:
arm-linux-objdump -S myboot
运行结果如图所示:
可以看到,起始地址和led2_on
的地址都没有问题。
得到了可执行文件,但是由于myboot
的文件较大,需要生成纯二进制.bin
文件。使用objcopy
命令:
arm-linux-objcopy -O binary myboot myboot.bin
然后,添加HeaderInfo
信息:
./mkv210 u-boot.bin u-boot.16k
最后烧写到SD
卡中:
sudo dd iflag=dsync oflag=dsync if=u-boot.16k of=/dev/sdb seek=1
总结起来:
arm-linux-gcc -c mystart.s
arm-linux-gcc -c mylowlevel_init.s //.s文件生成.o文件
arm-linux-ld -T myboot.lds -o myboot mystart.o mylowlevel_init.o //.o文件生成可执行文件
arm-linux-objcopy -O binary myboot myboot.bin //只保留二进制文件
./mkv210 u-boot.bin u-boot.16k //添加HeaderInfo
sudo dd iflag=dsync oflag=dsync if=u-boot.16k of=/dev/sdb seek=1 //烧写到SD卡
可以看到,如果讲需求改成点亮LED3
的话,只需要修改mylowlevel_init.s
即可。但是这些命令都还要再次输入一遍,这是很麻烦的事情!
有没有什么简单的办法呢?这就是make
的功能了,请看下一篇博文。