上一篇文章,讲述了制作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文件怎么链接成一个可执行文件呢?
.o链接成可执行文件
LDS文件
当对一个.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的功能了,请看下一篇博文。