【ARM裸机开发】如何编写Makefile和链接脚本

1.Makefile基本语法

ARM裸机开发需要在Ubuntu下进行编译,因此首先需要在Ubuntu环境下安装交叉编译器 arm-linux-gnueabihf-gcc 来编译。编译最后需要得到一个**.bin**文件,这个文件才可以下载到开发板中运行。
在 Linux 下用的最多的是 GCC 编译器,这是个没有 UI 的编译器,因此 Makefile 就需要我们自己来编写了。GCC 编译器的编译流程是:预处理、编译、汇编和链接。预处理就是展开所有的头文件、替换程序中的宏、解析条件编译并添加到文件中。编译是将经过预编译处理的代码编译成汇编代码,也就是我们常说的程序编译。汇编就是将汇编语言文件编译成二进制目标文件。链接就是将汇编出来的多个二进制目标文件链接在一起,形成最终的可执行文件,链接的时候还会涉及到静态库和动态库等问题。

1.1.Makefile 的引入

这样一个小工程,通过键盘输入两个整形数字,然后计算他们的和并将结果显示在屏幕上,在这个工程中我们有 main.c、input.c 和 calcu.c 这三个 C 文件和 input.h、calcu.h 这两个头文件。其中 main.c 是主体,input.c 负责接收从键盘输入的数值,calcu.c 进行任意两个数相加。(具体代码参考《正点原子-I.MX6U 嵌入式 Linux 驱动开发指南 V1.81》中142页
接下来对其进行编译,在终端输入如下命令:

1 gcc main.c calcu.c input.c -o main 

上面命令的意思就是使用 gcc 编译器对 main.c、calcu.c 和 input.c 这三个文件进行编译,编译生成的可执行文件叫做 main。编译完成以后执行 main 这个程序,执行结果如下:
在这里插入图片描述
如果工程有几万个文件,最好的办法肯定是哪个文件被修改了,只编译这个被修改的文件即可,其它没有修改的文件就不需要再次重新编译了。
如果第一次编译工程,我们先将工程中的文件都编译一遍,然后后面修改了哪个文件就编译哪个文件,命令如下:

1 gcc -c main.c 
2 gcc -c input.c 
3 gcc -c calcu.c 
4 gcc main.o input.o calcu.o -o main  

第一行到第三行将 main.c、input.c 和 calcu.c 编译成对应的.o 文件,所以使用了“-c”选项,“-c”选项,是只编译不链接
第四行命令是将编译出来的所有.o 文件链接成可执行文件 main。
假如我们现在修改了 calcu.c 这个文件,只需要将 caclu.c 这一个文件重新编译成.o 文件,然后在将所有的.o 文件链接成可执行文件即,只需要下面两条命令即可:

1 gcc -c calcu.c 
2 gcc main.o input.o calcu.o -o main 

但是这样就又有一个问题,如果修改的文件一多,我自己可能都不记得哪个文件修改过了,然后忘记编译,为此我们需要这样一个工具:
1、如果工程没有编译过,那么工程中的所有.c 文件都要被编译并且链接成可执行程序。
2、如果工程中只有个别 C 文件被修改了,那么只编译这些被修改的 C 文件即可。
3、如果工程的头文件被修改了,那么我们需要编译所有引用这个头文件的 C 文件,并且链接成可执行文件。
很明显,能够完成这个功能的就是 Makefile 了,在工程目录下创建名为“Makefile”的文件,文件名一定要叫做“Makefile”!!!大小写不能错。Makefile 和 C 文件是处于同一个目录的,在 Makefile 文件中输入如下代码:

1  main: main.o input.o calcu.o 
2      gcc -o main  main.o input.o calcu.o 
3  main.o: main.c 
4      gcc -c main.c 
5  input.o: input.c 
6      gcc -c input.c 
7  calcu.o: calcu.c 
8      gcc -c calcu.c 
9   
10 clean: 
11     rm *.o 
12     rm main

上述代码中所有行首需要空出来的地方一定要使用“TAB”键!不要使用空格键!这是Makefile 的语法要求

1.2.Makefile 的基本语法

Makefile 里面是由一系列的规则组成的,这些规则格式如下:

1 	目标…... :  依赖文件集合…… 
2 		命令 1 
3 		命令 2 
…… 

命令列表中的每条命令必须以 TAB 键开始,不能使用空格!
以如下代码为例进行分析,示例代码

1  main: main.o input.o calcu.o 
2      gcc -o main  main.o input.o calcu.o 
3  main.o: main.c 
4      gcc -c main.c 
5  input.o: input.c 
6      gcc -c input.c 
7  calcu.o: calcu.c 
8      gcc -c calcu.c 
9   
10 clean: 
11     rm *.o 
12     rm main

上述代码中一共有 5 条规则,1~2 行为第一条规则,3~4 行为第二条规则,5~6 行为第三条规则,7~8 行为第四条规则,10~12 为第五条规则。make 命令在执行这个 Makefile 的时候其执行步骤如下:
首先更新第一条规则中的 main,第一条规则的目标成为默认目标,只要默认目标更新了那么就认为 Makefile 的工作。在第一次编译的时候由于 main 还不存在,因此第一条规则会执行,第一条规则依赖于文件 main.o、input.o 和 calcu.o 这个三个.o 文件,这三个.o 文件目前还都没有,因此必须先更新这三个文件。make 会查找以这三个.o 文件为目标的规则并执行。以 main.o 为例,发现更新 main.o 的是第二条规则,因此会执行第二条规则,第二条规则里面的命令为“gcc –c main.c”,这行命令很熟悉了吧,就是不链接编译 main.c,生成 main.o,其它两个.o 文件同理。最后一个规则目标是 clean,它没有依赖文件,因此会默认为依赖文件都是最新的,所以其对应的命令不会执行。
当我们想要执行 clean 的话可以直接使用命令“make clean”,执行以后就会删除当前目录下所有的.o 文件以及 main,因此 clean 的功能就是完成工程的清理。

跟 C 语言一样 Makefile 也支持变量的,先看一下前面的例子:

main: main.o input.o calcu.o 
gcc -o main  main.o input.o calcu.o 

上述 Makefile 语句中,main.o input.o 和 calcue.o 这三个依赖文件,我们输入了两遍,我们这个 Makefile 比较小,如果 Makefile 复杂的时候这种重复输入的工作就会非常费时间,而且非常容易输错,为了解决这个问题,Makefile 加入了变量支持。不像 C 语言中的变量有 int、char 等各种类型,Makefile 中的变量都是字符串!类似 C 语言中的宏。使用变量将上面的代码修改,修改以后如下所示:

1 	objects = main.o input.o calcu.o 
2 		main: $(objects) 
3       gcc -o main $(objects)

Makefile 中变量的引用方法是“$(变量名)”,比如本例中的“$(objects)”就是使用变量 objects。
Makefile 变量的赋值符还有其它两个“:=”和“?=”,我们来看一下这三种赋值符(“=”、“:=”和“?=”)的区别。
赋值符“=”
示例代码:

1 name = zzk 
2 curname = $(name) 
3 name = zuozhongkai 
4  
5 print: 
6     @echo curname: $(curname)

运行结果:
在这里插入图片描述

借助另外一个变量,可以将变量的真实值推到后面去定义。变量的真实值取决于它所引用的变量的最后一次有效值。

赋值符“:=”
示例代码:

1 name = zzk 
2 curname := $(name) 
3 name = zuozhongkai 
4  
5 print: 
6     @echo curname: $(curname) 

运行结果:
在这里插入图片描述
赋值符“:=”不会使用后面定义的变量,只能使用前面已经定义好的,这就是“=”和“:=”两个的区别。

赋值符“?=”
“?=”是一个很有用的赋值符,比如下面这行代码:

curname ?= zuozhongkai 

上述代码的意思就是,如果变量 curname 前面没有被赋值,那么此变量就是“zuozhongkai”,如果前面已经赋过值了,那么就使用前面赋的值。

变量追加“+=”
Makefile 中的变量是字符串,有时候我们需要给前面已经定义好的变量添加一些字符串进
去,此时就要使用到符号“+=”,比如如下所示代码:

objects = main.o inpiut.o 
objects += calcu.o 

一开始变量 objects 的值为“main.o input.o”,后面我们给他追加了一个“calcu.o”,因此变量 objects 变成了“main.o input.o calcu.o”,这个就是变量的追加。

如果工程中 C 文件很多的话显然不能这么做。为此,我们可以使用 Makefile 中的模式规则,通过模式规则我们就可以使用一条规则来将所有的.c 文件编译为对应的.o 文件。模式规则中,至少在规则的目标定定义中要包涵“%”,否则就是一般规则,目标中的“%” 表示对文件名的匹配,“%”表示长度任意的非空字符串,比如“%.c”就是所有的以.c 结尾的文件,类似与通配符,a.%.c 就表示以 a.开头,以.c 结束的所有文件。
模式规则中,目标和依赖都是一系列的文件,每一次对模式规则进行解析的时候都会是不同的目标和依赖文件,而命令只有一行,如何通过一行命令来从不同的依赖文件中生成对应的目标?自动化变量就是完成这个功能的!所谓自动化变量就是这种变量会把模式中所定义的一系列的文件自动的挨个取出,直至所有的符合模式的文件都取完,自动化变量只应该出现在规则的命令中。

自动化变量描述
$@规则中的目标集合,在模式规则中,如果有多个目标的话,“$@”表示匹配模式中定义的目标集合。
$%当目标是函数库的时候表示规则中的目标成员名,如果目标不是函数库文件,那么其值为空。
$<依赖文件集合中的第一个文件,如果依赖文件是以模式(即“%”)定义的,那么“$<”就是符合模式的一系列的文件集合。
$?所有比目标新的依赖目标集合,以空格分开。
$^所有依赖文件的集合,使用空格分开,如果在依赖文件中有多个重复的文件,“$^”会去除重复的依赖文件,值保留一份。
$+和“$^”类似,但是当依赖文件存在重复的话不会去除重复的依赖文件。
$*这个变量表示目标模式中"%"及其之前的部分,如果目标是 test/a.test.c,目标模式为 a.%.c,那么“$*”就是 test/a.test。

常用的三种:$@$<$^,我们使用自动化变量来完成“示例代码”中的 Makefile:

1  objects = main.o input.o calcu.o 
2  main: $(objects) 
3      gcc -o main $(objects) 
4   
5  %.o : %.c 
6      gcc -c $< 
7   
8  clean: 
9      rm *.o 
10     rm main 

Makefile 有一种特殊的目标——伪目标,一般的目标名都是要生成的文件,而伪目标不代表真正的目标名,在执行 make 命令的时候通过指定这个伪目标来执行其所在规则的定义的命令。
使用伪目标主要是为了避免 Makefile 中定义的执行命令的目标和工作目录下的实际文件出现名字冲突,有时候我们需要编写一个规则用来执行一些命令,但是这个规则不是用来创建文件的。
上述规则中并没有创建文件 clean 的命令,因此工作目录下永远都不会存在文件 clean,当我们输入“make clean”以后,后面的“rm *.o”和“rm main”总是会执行。可是如果我们“手贱”,在工作目录下创建一个名为“clean”的文件,那就不一样了,当执行“make clean”的时候,规则因为没有依赖文件,所以目标被认为是最新的,因此后面的 rm 命令也就不会执行,我们预先设想的清理工程的功能也就无法完成。为了避免这个问题,我们可以将 clean 声明为伪目标,声明方式如下:

.PHONY : clean 

使用伪目标来更改示例代码 :

1  objects = main.o input.o calcu.o 
2  main: $(objects) 
3      gcc -o main $(objects) 
4   
5  .PHONY : clean 
6   
7  %.o : %.c 
8      gcc -c $< 
9   
10 clean: 
11     rm *.o 
12     rm main 

上述代码第 5 行声明 clean 为伪目标,声明 clean 为伪目标以后不管当前目录下是否存在名为“clean”的文件,输入“make clean”的话规则后面的 rm 命令都会执行。

Makefile的内容还有很多,这里只是例举一些基本的应用,能够编译ARM裸机开发中所写的文件即可。

2.学如何编写Makefile和链接脚本

在上一篇文章中,已经把点亮LED灯的.c文件和.h文件都编写好了,这里就需要通过编写makefile和链接脚本,将.c文件,编译成开发板能够运行的.bin文件。

2.1.Makefile

在文件目录下新建一个Makefile文件,同时在文件中输入如下代码:

1  objs := start.o main.o 
2   
3  ledc.bin:$(objs) 
4   arm-linux-gnueabihf-ld -Ttext 0X87800000 -o ledc.elf $^ 
5   arm-linux-gnueabihf-objcopy -O binary -S ledc.elf $@ 
6   arm-linux-gnueabihf-objdump -D -m arm ledc.elf > ledc.dis 
7    
8  %.o:%.s 
9   arm-linux-gnueabihf-gcc -Wall -nostdlib -c  -o $@ $< 
10   
11 %.o:%.S 
12  arm-linux-gnueabihf-gcc -Wall -nostdlib -c  -o $@ $< 
13   
14 %.o:%.c 
15  arm-linux-gnueabihf-gcc -Wall -nostdlib -c  -o $@ $< 
16   
17 clean: 
18  rm -rf *.o ledc.bin ledc.elf ledc.dis 

第 1 行定义了一个变量 objs,objs 包含着要生成 ledc.bin 所需的材料:start.o 和 main.o,也就是当前工程下的 start.s 和 main.c 这两个文件编译后的.o 文件。
这里要注意 start.o 一定要放到最前面!因为在后面链接的时候 start.o 要在最前面,因为 start.o 是最先要执行的文件!
第 3 行就是默认目标,目的是生成最终的可执行文件 ledc.bin,ledc.bin 依赖 start.o 和 main.o 如果当前工程没有 start.o 和 main.o 的时候就会找到相应的规则去生成 start.o 和 main.o。比如start.o 是 start.s 文件编译生成的,因此会执行第 8 行的规则。
第 4 行是使用 arm-linux-gnueabihf-ld 进行链接,链接起始地址是 0X87800000,但是这一行用到了自动变量$^$^的意思是所有依赖文件的集合,在这里就是 objs 这个变量的值:start.o 和 main.o。链接的时候 start.o 要链接到最前面,因为第一行代码就是 start.o 里面的,因此这一行就相当于:

arm-linux-gnueabihf-ld -Ttext 0X87800000 -o ledc.elf start.o main.o 

第 5 行使用 arm-linux-gnueabihf-objcopy 来将 ledc.elf 文件转为 ledc.bin,本行也用到了自动变量$@$@的意思是目标集合,在这里就是“ledc.bin”,那么本行就相当于:

arm-linux-gnueabihf-objcopy -O binary -S ledc.elf ledc.bin 

第 6 行使用 arm-linux-gnueabihf-objdump 来反汇编,生成 ledc.dis 文件
第 8~15 行就是针对不同的文件类型将其编译成对应的.o 文件
第 17 行就是工程清理规则,通过命令“make clean”就可以清理工程。

2.2.链接脚本

在上面的 Makefile 中我们链接代码的时候使用如下语句:

arm-linux-gnueabihf-ld -Ttext 0X87800000 -o ledc.elf $^

上面语句中我们是通过“-Ttext”来指定链接地址是 0X87800000 的,这样的话所有的文件都会链接到以 0X87800000 为起始地址的区域。但是有时候我们很多文件需要链接到指定的区域,或者叫做段里面,比如在 Linux 里面初始化函数就会放到 init 段里面。因此我们需要能够自定义一些段,这些段的起始地址我们可以自由指定,同样的我们也可以指定一个文件或者函数应该存放到哪个段里面去。要完成这个功能我们就需要使用到链接脚本。

链接脚本的语法很简单,就是编写一系列的命令,这些命令组成了链接脚本,每个命令是一个带有参数的关键字或者一个对符号的赋值,可以使用分号分隔命令。像文件名之类的字符串可以直接键入,也可以使用通配符“*”。最简单的链接脚本可以只包含一个命令“SECTIONS”, 我们可以在这一个“SECTIONS”里面来描述输出文件的内存布局。我们一般编译出来的代码都包含在 text、data、bss 和 rodata 这四个段内,假设现在的代码要被链接到 0x10000000 这个地址,数据要被链接到 0x30000000 这个地方,下面就是完成此功能的最简单的链接脚本:

1 SECTIONS{ 
2   . = 0X10000000; 
3   .text : {*(.text)} 
4   . = 0X30000000; 
5   .data ALIGN(4) : { *(.data) }      
6   .bss ALIGN(4)  : { *(.bss) }     
7 }

第 1 行我们先写了一个关键字“SECTIONS”,后面跟了一个大括号,这个大括号和第 7 行的大括号是一对,这是必须的。看起来就跟 C 语言里面的函数一样。
第 2 行对一个特殊符号“.”进行赋值,“.”在链接脚本里面叫做定位计数器,默认的定位计数器为 0。我们要求代码链接到以 0X10000000 为起始地址的地方,因此这一行给“.”赋值0x10000000,表示以 0x10000000 开始,后面的文件或者段都会以 0X10000000 为起始地址开始链接。
第 3 行的“.text”是段名,后面的冒号是语法要求,冒号后面的大括号里面可以填上要链接到“.text”这个段里面的所有文件,“*(.text)”中的*是通配符,表示所有输入文件的.text 段都放到“.text”中。
第 4 行,我们的要求是数据放到 0x30000000 开始的地方,所以我们需要重新设置定位计数器“.”,将其改为 0x30000000。如果不重新设置的话会怎么样?假设“.text”段大小为 0x10000,那么接下来的.data 段开始地址就是 0x10000000+0x10000=0x10010000,这明显不符合我们的要求。所以我们必须调整定位计数器为 0x30000000。
第 5 行跟第 3 行一样,定义了一个名为“.data”的段,然后所有文件的“.data”段都放到这里面。但是这一行多了一个**“ALIGN(4)”,这是什么意思呢?这是用来对“.data”这个段的起始地址做字节对齐的,ALIGN(4)表示 4 字节对齐。也就是说段“.data”的起始地址要能被 4 整除**,一般常见的都是 ALIGN(4)或者 ALIGN(8),也就是 4 字节或者 8 字节对齐。
第 6 行定义了一个“.bss”段,所有文件中的“.bss”数据都会被放到这个里面,“.bss”数据就是那些定义了但是没有被初始化的变量。
LED灯的链接脚本要求如下:
①、链接起始地址为 0X87800000。
②、start.o 要被链接到最开始的地方,因为 start.o 里面包含这第一个要执行的命令。

1  SECTIONS{ 
2    . = 0X87800000; 
3    .text : 
4    { 
5         start.o  
6         main.o  
7         *(.text) 
8    } 
9    .rodata ALIGN(4) : {*(.rodata*)}      
10   .data ALIGN(4)   : { *(.data) }     
11   __bss_start = .;     
12   .bss ALIGN(4)  : { *(.bss)  *(COMMON) }     
13   __bss_end = .; 
14 } 

第 2 行设置定位计数器为0X87800000,因为我们的链接地址就是0X87800000。
第 5 行设置链接到开始位置的文件为start.o,因为 start.o 里面包含着第一个要执行的指令,所以一定要链接到最开始的地方。
第 6 行是 main.o 这个文件,其实可以不用写出来,因为 main.o 的位置就无所谓了,可以由编译器自行决定链接位置。
第 11、13 行有“__bss_start”和“__bss_end”这两个东西?这个是什么呢?“__bss_start” 和“__bss_end”是符号,第 11、13 这两行其实就是对这两个符号进行赋值,其值为定位符“.”,这两个符号用来保存.bss 段的起始地址和结束地址。前面说了.bss 段是定义了但是没有被初始化的变量,我们需要手动对.bss 段的变量清零的,因此我们需要知道.bss 段的起始和结束地址,这样我们直接对这段内存赋 0 即可完成清零。通过第 11、13 行代码,.bss 段的起始地址和结束地址就保存在了“__bss_start”和“__bss_end”中,我们就可以直接在汇编或者 C 文件里面使用这两个符号。

在已经编写好了链接脚本文件:imx6ul.lds,我们肯定是要使用这个链接脚本文件的,将 Makefile 中的如下一行代码:

arm-linux-gnueabihf-ld -Ttext 0X87800000 -o ledc.elf $^ 

改为:

arm-linux-gnueabihf-ld -Timx6ul.lds -o ledc.elf $^ 

其实就是将-T 后面的 0X87800000 改为 imx6ul.lds,表示使用 imx6ul.lds 这个链接脚本文件。修改完成以后使用新的 Makefile 和链接脚本文件重新编译工程,编译成功以后就可以烧写到 SD 卡中验证了。
使用软件 imxdownload 将编译出来的 ledc.bin 烧写到 SD 卡中,命令如下:

chmod 777 imxdownload //给予 imxdownload 可执行权限,一次即可 
./imxdownload ledc.bin /dev/sdd //烧写到 SD 卡中,不能烧写到/dev/sda 或 sda1 设备里面

这个代码是正点原子为IMX6ULL开发板专门写的,并不适用别的开发板。

3.总结

经过上面两部分的内容,再结合上一篇文章的代码文件,就可以点亮开发板上的一个LED灯了。那么一个完整的ARM裸机的工程文件会包含如下部分:

在这里插入图片描述

参考资料:
《IMX6ULL 参考手册》官方参考手册
《跟我一起写 Makefile》
《正点原子-I.MX6U 嵌入式 Linux 驱动开发指南 V1.81 》开发教程
开发板平台:正点原子 I.MX6U ALPHA

  • 24
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿强12138

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值