1. 交叉开发
什么是交叉编译?
项目的开发流程:
编辑代码 --> 编译代码 --> 运行及调试代码
第一阶段的所有编译工作都由gcc编译器完成。
如:
gcc main.c --> a.out
gcc main.c -o main --> main
交叉编译也是一种编译,也是将一种高级语言翻译成一种机器语言。
交叉 cross-compiling
在PC上面,我们可以做到编译环境(就是你写代码和编译代码的那个机器环境)和运行环境
(运行代码的那个机器环境)是相同的。
很多嵌入式的产品是不适合去写程序,也没有开发工具的。
有人就提出把上面的项目开发流程的那三个步骤分开:
就是写程序,编译程序我们还是放在PC上来完成
---> 也就是编译环境还是由PC来提供
最终运行环境我们放在目标板上来完成
---> 也就是说运行环境由目标板来提供
所以,交叉编译就是在一种环境(x86)下将代码编译成能在另一种环境(ARM)下运行的程序。
那么我们通过另一条交叉编译指令来进行编译,如:
arm-linux-gcc hello.c
or
arm-linux-gcc hello.c -o hello
请注意:
这条指令并不是Ubuntu这个linux操作系统自带的,而是通过交叉编译工具链得来的。
交叉编译工具链一般是由厂家来进行提供,我们直接拿来用即可。
交叉编译工具链的移植步骤如下:
1) 把交叉编译工具链的压缩包,拷贝到你的linux系统下。
如:
在window下cp<gcc-linaro-5.5.0-2017.10-x86_64_arm-linux-gnueabi.>到share目录下。
2) 解压到一个合适的目录下(不能是共享目录)
如:
sudo mkdir -p /usr/local/arm
sudo tar -xvf gcc-linaro-5.5.0-2017.10-x86_64_arm-linux-gnueabi. -C /usr/local/arm
3) 把交叉编译的命令的完整的路径加入到环境变量PATH中去
有两种方式:
1. 在终端上输入指令:
export PATH=$PATH:/usr/local/arm/5.4.0/usr/bin
上述方式只是临时有效。
2. 把上面这条指令加入到~/.bashrc的最后一行
vim ~/.bashrc
如上方式永久有效
二、开发板的下载方式
you的电脑需要通过SecureCRT先连接上开发板
1) 通过串口下载文件到开发板中去
输入指令:
rx 要接受的文件名(这是通过xModem串口协议来接收文件)
如:
rx a.out
接着点击 上方菜单栏中的 “传输”
--> 在下拉的选项中点击"发送xModem"
--> 在选项框中选中你要下载的文件。
--> 接着等待下载完成即可。
2) 通过tftp下载
1. 先连接网线至路由器上
2. 网络设置
a. 先设置虚拟机的网络(IP地址)
我们可以在linux上输入ifconfig指令查看当前设备的IP地址:192.168.31.72
在window下调出命令台,输入ipconfig查看PC的IP地址:192.168.31.121
虚拟机 --> 设置 --> 网络适配器 --> 桥接模式
桥接模式:
相当于将虚拟机作为一台独立的机器,虚拟机也将会有一个独立的与主机不
一致的IP地址。
接下来到Ubuntu中设置IP:
系统设置 --> 网络 --> '+' --> 配置IP地址
b. ubuntu中tftpd服务器的配置
安装tftpd:
sudo apt-get install tftpd-hpa
tftp的控制命令:
启动:sudo service tftpd-hpa start
重启:sudo service tftpd-hpa restart
停止:sudo service tftpd-hpa stop
配置tftp的服务:
tftp配置文件在/etc/default/tftpd-hpa。
c. 配置开发板的IP
在开发板的/etc/init.d目录下,新建一个文件Start.sh
系统每次重启的时候,都会自动执行/etc/init.d/Start.sh
Start.sh中的内容为:
#注意:xxx为分配给你的开发板的IP地址
#! /bin/bash
ifconfig eth0 down
ifconfig eth0 hw ether 08:09:00:A0:25:xxx
ifconfig eth0 192.168.31.xxx netmask 255.255.255.0 up
telnetd &
配置完成后,保存并退出,输入reboot或者按重启键重启开发板,重启完成后输入ifconfig
看开发板的IP是否修改成功。
d. tftp命令下载或上传文件到服务器
在开发板上运行命令,就可以下载或上传文件啦。
tftp -g -r ex.c 192.168.31.185
通过tftp的网络服务从服务器192.168.31.185上获取(-g :get)一个远程的文件
(-r remote)文件名为ex.c
tftp -p -l 1.mp3 192.168.31.185
同伙tftp的网络服务上传(-p put)一个本地文件(-l local)文件名为1.mp3到
192.168.31.185的服务器上面去.
三、GCC编译过程
之前我们在执行编译指令时,都是通过:
gcc main.c -o main
or
arm-linux-gcc main.c -o main
如果有多个文件的话,那么则:
gcc main.c 1.c 2.c -o main
or
arm-linux-gcc main.c 1.c 2.c -o main
编译其实是分为很多步的,详情请见图<编译过程>。
如果gcc命令后面不跟任何选项的话,会默认执行预处理、编译、汇编、链接整个过程。
如果你的程序没有语法问题的话,将会默认生成一个a.out可执行文件。
1) 预处理(preprocessing)
预处理就是处理程序中以"#"开头的行,换句话说就是只要是以"#"开头的行就是预处理指令。
预处理如何进行?
只需要在gcc指令的后面加上-E选项即可
-E: 提示编译器执行完预处理就停下来,后面的编译、汇编、链接不执行。
gcc -E hello.c -o hello.i
预处理指令有以下三种:
a. #include 文件包含命令
#include ""
#include <>
b. #define 宏定义
仅替换
c. 条件编译
#if xxxx #endif
#if yyyy #else #endif
#ifdef xxx #endif
#ifndef xxx #define xxx #endif
由上例子可知,预处理进行的操作:
1. 将所有的#define删除,并且展开所有的宏定义
2. 处理所有的条件编译指令
3. 处理#include预编译指令,将被包含的头文件插入到该编译指令的位置
4. 删除掉所有的注释 "//" "/**/"
5. 添加行号和文件名标识,方便后面编译时编译器产生调试用的行号以及编译时
产生编译错误或警号时能够显示行号。
2) 编译(compiling)
使用-S选项,编译编译操作执行完就结束。对应生成一个.s文件
编译过程是整个程序构建的核心部分,编译成功,会将源代码由文本形式转换成汇编语言
(arm-liux-)gcc -S hello.i(hello.c) -o hello.s
3) 汇编(assembling)
把汇编程序汇编成一个目标程序(机器指令文件)。每一个汇编语句几乎都对应一个机器指令
对应生成一个.o文件
(arm-linux-)gcc -c hello.s -o hello.o
(arm-linux-)as hello.s -o hello.o(as是汇编器)
hello.o是二进制文件,但是还不能执行。
4) 链接(linking)
就是把各目标文件.o链接成一个可执行文件
把各个目标文件的.test .data.....合在一起,解决地址中的冲突问题
(arm-linux-)gcc hello.o ... 1.o 2.o -o hello
四、GDB调试程序
GDB是一个单步调试的命令行工具,它可以使你的程序,在你输入命令后,才运行。
而且可以打印中间变量的值。
(1) gcc -g main.c -o main
-g表示在编译过程中,加入调试信息,以便于GDB来调试。
(2) 调试程序
gdb main
GDB的调试命令:
(gdb)b
break 用来设置断点,什么是断点?
程序执行到此处时,会暂停,等待用户的输入命令后,才继续运行。
b line_num ---> 设置line_num行号为一个断点
b 符号(函数)---> 设置函数为一个断点
(gdb)info b
查看断点的情况
(gdb)delete breakpoints 断点号(clear n代表删除第n行的断点)
删除断点
(gdb)r
run 运行代码
(gdb)p n
print 打印变量n的值
(gdb)n
next 下一步 单步运行
(gdb)s
step 下一步 单步运行
n和s的区别,仅在函数调用时有区别:
n把函数调用当做是一步,直接把调用函数运行完
s:step into 会进入函数一步一步的执行
(gdb)display命令
display sum
跟踪查看sum变量的值,每次停下来都会显示它的值
(gdb)l
list 查看源文件
(gdb)help
查看gdb命令的帮助文件
(gdb)quit
退出
五、动态库和静态库
1) 库
库是一种代码的二进制的封装形式,可以理解为库就是很多.c文件预先编译好的二进制代码
的一种函数集合。在其他的源文件中你可以调用库,但是你可能并不知道它到底是如何实现
的。
linux下的库主要出于/lib和/usr/lib下。
library : 库
/lib 下的是与系统运行相关的库文件
/usr/lib 下的一般是指第三方软件的库文件
库又分为动态库和静态库
2) 动态库
动态库的使用步骤:
a. 编辑源代码
xxxx.c 功能函数的实现
xxxx.h 函数以及数据等的声明 "接口文件"
....
b. 生成动态库
(arm-linux-)gcc -shared -fpic -o libxxxx.so xxxx.c
-shared : 生成动态库(共享库)
-fpic : 与位置无关
-o : 指定生成的动态库的名字
约定动态库的名字以lib为开头,以.so为结尾
xxx.c :生成的动态库的C源代码文件列表
c. 把头文件和库拷贝给需要的人
拷贝完头文件和库之后,编写代码:
#include "sum_lss.h"
#include <stdio.h>
int main()
{
int a = sum_lss(5,3);
printf("sum = %d\n",a);
return 0;
}
编写完代码之后,进行编译工作时要注意添加两个如下选项:
-I(include) 后面接一个目录名,指定头文件的搜索路径,可以接多个-I
-L(lib) 指定一个库的搜索路径,可以接多个-L
-l 指定一个需要用到的库的名字(除去前面的lib和后面的.so)可以接多个-l
比如:
libabc.so ---> dir1
libxyz.so ---> dir1
xxxx.h ---> dir2
-L dir1 -l abc -l xyz -I dir2
libabc.so --> dir1
libxyz.so --> dir2
-L dir1 -l abc -L dir2 -l xyz
如:
(arm-linux-)gcc main.c -I ./inc -L ./lib -l sum_lss -o main
我们生成了一个可执行文件main,并且这个可执行文件main中会使用动态库中
函数(代码)。但是,在生成main的时候,并没有把动态库libsum_lss.so的函数
(代码)拷贝到可执行文件main中去,它只是标记了一下main这个可执行文件
需要用到libsum_lss.so这个库里面的相关功能代码。
当真正运行main的时候,它会去找它所依赖的库:
./main
./main: error while loading shared libraries:
libsum_lss.so: cannot open shared object file: No such file or directory
d. 运行时刻,需要指定动态库的搜索路径
在linux系统下,有一个环境变量LD_LIBRARY_PATH(load)
LD_LIBRARY_PATH:加载库的路径
系统就会在变量LD_LIBRARY_PATH指定的目录下去搜索所依赖的库
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:dir1
接着运行即可
./main
3) 静态库
静态库的使用步骤:
a. 编辑库的源代码
a.c(a.h) --> int sum_v1(int a,int b);
b.c(b.h) --> int sub_v1(int a,int b);
....
b. 把各源文件编成.o文件
a.c --> a.o
(arm-linux-)gcc -c a.c -o a.o
b.c --> b.o
(arm-linux-)gcc -c b.c -o b.o
.....
c. 用ar命令把所有的.o文件生成一个静态库
(arm-linux-)ar -rc libyyy.a a.o b.o
把a.o 和 b.o 打包生成一个静态库,这个静态库的名称为yyy
一般约定静态库的名称以lib开头,以.a结尾。
d. 把头文件和静态库拷贝给需要的人
接着进行编译:
(arm-linux-)gcc main.c -I ./inc -L ./lib -l yyy -o main
需要注意的是:
静态库有区别于动态库,当使用静态库时,编译可执行文件的时候,会把静态库中
的内容编进可执行文件中去。
e. 直接运行即可。
因为在编译的时候已经把静态库的内容编写进了可执行文件中去了,所以执行的时候
不需要再去搜索所依赖的库,直接运行即可。
动态库和静态库的区别和相同点:
相同点:两种库都是二进制的一种封装形式
不同点:
1. 对于动态库,编译可执行文件时,并没有把动态库的内容copy到可执行文件中去
只是做了一个标记,表示可执行文件需要用到xxx动态库。这样的话,运行可执行文件
时,会先去加载可执行文件所依赖的动态库。
对于静态库,编译可执行文件时,会把静态库中的内容copy到可执行文件中去,运行
可执行文件时,就不需要再去加载静态库了,但是这样也会导致最终可执行文件的代码量
相对增多。相当于编译器将代码补充完整了,这样运行起来就会相对于快一点,但是有一个
缺点:占用我们的磁盘和内存空间
所以总的来说就是静态库牺牲了空间效率,换取了执行效率。
2. 当函数接口不变时,更新动态库,不需要重新编译依赖该动态库的工程文件
如果是静态库,修改,则需要重新编译依赖该静态库的工程文件。
六、Makefile
1) 什么是Makefile
Makefile是make的配置文件。
make是一个智能化的自动编译的工程管理工具。
它可以决定在一个工程里,哪些文件需要被编译,哪些不需要被编译,哪些可以先编译
哪些可以后编译,而且他知道如何去编译他们。
make如何去做是有依据的,那么make的依据来源于Makefile。
Makefile告诉make去做什么以及如何去做,也就是说Makefile关系到整个工程的编译规则。
Makefile它是make的配置文件,它是一个文本文件,一旦Makefile写好,此时只需要一个
make命令,整个工程将依据Makefile中的编译规则自动编译,极大的提高了项目开发的效率。
那么会不会写Makefile,从侧面说明了一个人是否具备完成大型工程的能力。
Makefile作为一个文本文件,那这个文本文件该如何编辑?
2) Makefile文件格式
a. 文件名
Makefile or makefile
b. 文件格式
TARGET:PREREQUISITES//表示依赖关系
<Tab键>COMMAND1//每一条命令的开头必须以Tab键开头
<Tab键>COMMAND2
....
<Tab键>COMMANDn
TARGET:目标名,通常是要生成的文件名
PREREQUISITES:依赖文件列表
当需要生成某个"TARGET"时,首先是看当前目录下是否已经有依赖文件,如果没有
就看Makefile中是否有以依赖文件为目标,如果有,就去生成它。
一个Makefile文件中通常包含一个或多个“Rules”-->目标。
c. 运行make
make 目标名
make首先要在当前目录下去找Makefile/makefile,然后再到该文件中去找"目标名"
,再找它的依赖文件...再执行达到此目的命令。
make -f dir 目标名
make首先会在指定的目录dir下去找Makefile/makefile......
make
make首先在当前目录下去找Makefile/makefile,然后再到该文件中去找“第一个”
目标名,再去找它的依赖文件.....再执行达到此目的命令。
d. 最简单的一个Makefile
首先要链接一个Makefile中的变量,在Makefile中可以定义变量,但是Makfile中额
变量时没有数据类型的,都是当成字符串来处理。
在Makefile中利用#作为行注释,类似于C语言中的//
引用变量:$(变量名)
引用变量 --> 代表的变量的值
a. 简单赋值 :=
会在变量的定义点,按照被引用的变量的当前值进行展开,不会向后展开。
例子:
B := $(A)
A := "12345"
那么$(B) --> 为空
b. 递归赋值 =
如果变量的定义引用了其他变量,那么引用会一直展开下去,直到找到被引用的
变量的最新的定义,也就是说会向后展开
例子:
B = $(A)
A := "12345"
那么$(B) --> "12345"
c. 追加赋值 +=
当这个变量以前没有被定义过,+=和=是一样的。但是,当变量已经有定义的时候
+=只是简单的进行字符的添加工作
A := 123
A += 456 --> A := $(A)456 ---> A := 123456
d. 条件赋值 ?=
当变量没有定义或没有值(定义了但是没有初始化),我们才给它赋值
CC ?= arm-linux-gcc
如上是我们人为的去定义变量,在我们的Makefile中还有一些自动变量,
自动变量-->早就有人帮我们定义好的变量
自动变量是make内置的变量,make赋予这些变量一个特殊的含义:
$@ 目标文件的完整名称
$+ 所有依赖文件名,以空格分开,可能会包含有重复的依赖文件
$^ 所有不重复的依赖文件名,每一个文件之间以空格隔开
$< 第一个依赖文件的名称
$? 所有时间戳比目标文件晚(新)的依赖文件,并以空格分开
例子:
TARGET := display #目标文件名
CC := arm-linux-gcc #CC指定的编译器
$(TARGET):a.o b.o c.o d.o #依赖关系
$(CC) $^ -o $@ #生成规则,注意前面需要一个Tab键
a.o:a.c #依赖文件的生成规则
$(CC) -c $< -o $@
b.o:b.c
$(CC) -c $< -o $@
c.o:c.c
$(CC) -c $< -o $@
d.o:d.c
$(CC) -c $< -o $@
通配符:
%.o:%.c(%表示任意多个字符,%应用于Makefile,*应用于系统中)
$(CC) -c $< -o $@
Makefile函数
Makefile允许定义并调用函数:
在Makefile中,调用内置的函数或者自定义的函数的格式如下:
$(函数名 函数参数列表)
例子:
wildcard
文件名展开函数,展开成一列所有符合由其参数描述的文件名,
文件名之间以空格分隔开来,函数调用成功将会返回展开后的文件名列表。
wildcard这个函数只带一个参数,指定文件名的通配方式。
wildcard就是看你的文件名是否符合我参数定义的规则。
如:
$(wildcard *.c)
patsubst
模式字符串替换函数
$(patsubst <from>,<to>,<text>)
把原始字符串<text>中的<from>替换成<to>
$(patsubst %.c,%.o,"1.c 2.c")
--> 1.o 2.o