树莓派学习笔记(五):交叉编译

交叉编译

交叉编译的概念

所谓编译,是指根据编辑好的源代码,生成本平台可直接执行的文件(比如gcc编译C语言,默认生成的a.out,使用./a.out即可运行),这种方式适用于本平台有足够的资源和条件时使用。

但当目标平台没有充足的资源,无法安装、运行编译工具,甚至操作系统时,我们必须先在有条件的平台上进行编译,将可执行文件复制到目标平台运行

比如C51单片机内存极小,根本无法运行操作系统,更别提安装编译器了。其主板运行的机器指令等,也和windows不同。质我们总是在windows平台上的IDE:Keil中编写源代码,编译生成hex文件,再通过stc-isp工具将其下载(烧写)到单片机上。

那么,树莓派内存空间大,可以运行Ubuntu、Debian、Android甚至windows,也需要交叉编译吗?

答案是肯定的。要知道,操作系统本身也是软件,也需要源代码编译生成。当工程师刚刚设计好硬件部分,此时树莓派还没有运行操作系统的能力。一个平台/主机的运行,至少需要两样东西:bootloader和操作系统核心。因此要进行产品测试,编译适合树莓派运行,适配树莓派硬件特点的操作系统版本后,产品才能发布上市。

交叉编译涉及到的两个基本对象:宿主机(host)和目标机(target),其中

  • 宿主机指编辑和编译程序的平台,一般是基于X86的PC机,通常也被称为主机。
  • 目标机是用户开发的系统,通常都是非X86平台。host编译得到的可执行代码在target上运行

交叉编译工具链的安装

不同平台使用的指令集不同,交叉编译工具链也就不同。因此目标平台是什么,在宿主机平台上编译的时候就要选择正确(比如C51课程阶段Keil创建工程时就要选择目标设备为AT89C52)

树莓派平台的交叉编译,由树莓派官方提供了工具链,源码地址:raspberry-tools。通过git clone到本地(宿主机,编译的机器,这里我用的是另一台ubuntu64位系统的ECS)后,进入如下目录:tools-master/arm-bcm2708

如果宿主机是64位版本选择gcc-linaro-arm-linux-gnueabihf-raspbian-x64,如果是32位选择gcc-linaro-arm-linux-gnueabihf-raspbian

以64位系统为例,其提供的gcc程序为bin/arm-linux-gnueabihf-gcc-4.8.3

我们使用tools-master/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin/arm-linux-gnueabihf-gcc-4.8.3即可调用4.8.3版本的的gcc进行编译

如果嫌上面的路径太长太深,不愿意一步一步进到具体路径,可以设置环境变量:

  • 修改环境变量:export PATH=$PATH:上面的gcc程序路径写到bin,便可直接在任意目录使用arm-linux-gnuabihf-gcc命令:

  • echo export PATH=$PATH >> ~/.bashrc;source ~/.bashrc ,将临时的环境变量写入文件这样就不会丢失

交叉编译小实例:基于socket的聊天应用编译

在Linux系统编程课程阶段,我们学习了网络编程,这里以基于socket实现的聊天服务器(及其客户端)为例,在宿主机(ubuntu18.04)上交叉编译可在树莓派运行的可执行文件

首先是源码,服务端:

点击展开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
//server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int s_fd;
int c_fd;
int n_read;
char readBuf[128];
char msg[128]={0};
int mark=0;

struct sockaddr_in s_addr;
struct sockaddr_in c_addr;

//1. socket
s_fd = socket(AF_INET, SOCK_STREAM, 0);
if(s_fd == -1){
perror("socket");
exit(-1);
}

s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&s_addr.sin_addr);
//注意格式,不能使用s_addr.sin_addr.inet_aton(argv[1])……

//2. bind
bind(s_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));

//3. listen
listen(s_fd,10);
//4. accept
int clen = sizeof(struct sockaddr_in);
while(1){
c_fd=accept(s_fd,(struct sockaddr*)&c_addr,&clen);
if(c_fd==-1){
perror("accept");
}
mark++;
printf("get connection, %s\n",inet_ntoa(c_addr.sin_addr));

if(fork() == 0){
if(fork()==0){//create child to input
while(1){
sprintf(msg,"No.%d client connection still ok",mark);
write(c_fd,msg,strlen(msg));
sleep(10);
}
}
while(1){
memset(readBuf,'\0',sizeof(char)*128);
n_read=read(c_fd,readBuf,128);
if(n_read==-1){
perror("read");
} else if(n_read>0){
printf("\nget msg:\n<<<%s\r\n",readBuf);
}
}
break;
}
}
return 0;
}

客户端:

点击展开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//client.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char **argv)
{
int c_fd;
int n_read;
char readBuf[128];

char msg[128]={0};
struct sockaddr_in c_addr;

memset(&c_addr,0,sizeof(struct sockaddr_in));

if(argc != 3){
printf("param is not good\n");
exit(-1);
}

//1. socket
c_fd = socket(AF_INET, SOCK_STREAM, 0);
if(c_fd == -1){
perror("socket");
exit(-1);
}

c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&c_addr.sin_addr);
//2.connect
if(connect(c_fd, (struct sockaddr *)&c_addr,sizeof(struct sockaddr)) == -1){
perror("connect");
exit(-1);
}
printf("connected ...\n");
//int mark = 0;
while(1){
if(fork()==0){//子进程,一直输入
while(1){
memset(msg,0,sizeof(char)*128);
printf(">>>");
gets(msg);
write(c_fd,msg,strlen(msg));
}
}

while(1){//主进程,从服务器取消息
memset(readBuf,'\0',sizeof(char)*128);
n_read=read(c_fd,readBuf,128);
if(n_read == -1){
perror("read");
} else if(n_read>0) {
printf("\nget msg<<<%s\n\r",readBuf);
}
}return 0;
}
}

可以看到,两种gcc编译结果不同,使用arm-linux-gnueabihf-gcc编译得到的可执行文件目标平台为arm,才能在树莓派上运行。

接着在树莓派上使用scp命令将编译好的可执行文件下载到开发板,并运行:

1
2
scp username@ip:path-to-the-file target-path
./chat-client

运行效果如上所示

带wiringPi的交叉编译

前面在树莓派上利用wiringPi库开发过一些应用,那么假如我们树莓派的操作系统还没有准备好,就只能在宿主机上提前编译好。wiringPi也是如此,之前在树莓派上下载了wiringPi源码,并通过gcc,build了适合树莓派的特制版本,宿主机上要想获取wiringPi库进行相应外设开发,同样也需要下载源码、使用gcc进行build。

这就导致一个问题:树莓派上的gcc和宿主机上的gcc目标平台并不相同,宿主机上的gcc适用于x86平台,编译出的wiringPi库自然也只能用于x86的操作系统,那还怎么进行下一步的交叉编译呢?

wiringPiMakefile,指定gcc为编译器:

树莓派的gcc,目标平台是arm64:

宿主机编译安装的wiringPi,目标平台是x86-64:

如果强行使用x86平台的wiringPi进行交叉编译,会报错(即使强行去修改Makefile中的gcc,编译出来的版本依然不能用):

那这种情况下有两种思路:

第一种,“就地取材”,既然我们之前已经编译好了树莓派能用的wiringPi,直接将其拿到宿主机去用即可

  • 树莓派上执行scp /usr/lib/libWiringPi.so.xxx user@host:target-path,将动态库拷贝到宿主机
    • 可以使用ln -s target llinkname 的格式创建一个软链接,方便链接时名字的简洁。关于硬链接与软链接的概念和区别具体参考https://www.cnblogs.com/zhangna1998517/p/11347364.html,主要有以下几点:
    • 软链接是”真正的“链接,类似于一个不占磁盘空间的指针,只存放位置信息;硬链接像目标文件的备份,内容随目标文件变化而变化,相比软链接可以防止用户误删文件。硬链接通过索引节点实现,只有当文件及其所有硬链接都被删除,相应的磁盘空间才会被释放。
  • 在宿主机上使用arm-linux-gnueabihf-gcc file.c -I path-to-wiringPi -Lpath-to-library -lwiringPi -o target的命令进行交叉编译
  • 编译好后使用方式和上面socket聊天应用一样,复制到开发板直接运行即可。

第二种方式,更加彻底、符合常规:那就是在安装外设库的时候配置目标平台,但由于这个wiringPi并没有支持配置,因此没有展示


树莓派学习笔记(五):交叉编译
https://dockingyuan.top/2023/01/16/raspberry/5-交叉编译/
作者
Yuan Yuan
发布于
2023年1月16日
许可协议