目录结构说明
一、基础说明
1、环境介绍
2、linux的根文件结构
3、瑞士军刀busybox
4、busybox源码准备
二、静态编译并测试busybox
1、配置busybox静态编译选项
2、编译busybox
3、安装busybox
4、测试busybox
三、动态编译并测试busybox
1、配置、编译、安装busybox
2、测试busybox
四、构建根文件系统
1、根文件系统的目录
2、具体创建步骤
3、写成根文件系统创建脚本
五、无法解析域名
目录结构说明
一、基础说明
1、环境介绍
首先这里我们需要安装一些基础的编译环境,安装tftp服务器,安装nfs服务器等,这里我们可以参考文章《orangpione利用usb共享网络(RNDIS)实现tftp加载内核挂载到NFS根文件系统》中的环境准备章节,自行安装相关服务,在这里不展开。后续的开发是基于tftp和nfs传输的条件下进行,如果没有实现相关传输,也可以通过烧录sd卡的形式进行。
2、linux的根文件结构
linux的根文件目录遵循的FHS,而FHS是Filesystem Hierarchy Standard(文件系统层次化标准)的缩写,多数Linux版本采用这种文件组织形式,类似于Windows操作系统中c盘的文件目录,FHS采用树形结构组织文件。FHS定义了系统中每个区域的用途、所需要的最小构成的文件和目录,同时还给出了例外处理与矛盾处理。
这里如果我们想深入了解根文件系统的结构,我们可以到下面的访问网址https://refspecs.linuxfoundation.org/fhs.shtml,也可以从该网址下载具体的pdf说明文档,现在最新的文档是3.0的版本,地址是https://refspecs.linuxfoundation.org/FHS_3.0/fhs-3.0.pdf,文档中有各个目录的具体介绍,这里不详细介绍,后面我们制作根文件系统的时候,会对应的说明
3、瑞士军刀busybox
BusyBox 是一个集成了三百多个最常用Linux命令和工具的软件。BusyBox 包含了一些简单的工具,例如ls、cat和echo等等,还包含了一些更大、更复杂的工具,例grep、find、mount以及telnet。有些人将 BusyBox 称为 Linux 工具里的瑞士军刀。简单的说BusyBox就好像是个大工具箱,它集成压缩了 Linux 的许多工具和命令,也包含了 Android 系统的自带的shell。简单来说就是我们在linux系统中常用的命令都是可以从busybox里面编译集成的,比如cp、mkdir、ls、mv、rm、adduser、deluser等等
4、busybox源码准备
- 新建目录
mkdir mkrootfs
- 进入目录mkrootfs,并且下载busybox源码
我们从码云下载源码,上面的busybox是github上的源码镜像,每天更新一次
git clone https://gitee.com/mirrors/busyboxsource.git busybox
,源码下载好后,我们在源码的上层mul - 在mkrootfs目录下,新建脚本
vi build.sh
,这里主要是把busybox的编译输出到目录bboxbuild目录中,把busybox的安装到目录rootfs中。脚本的ARCH和CROSS_COMPILE定义了相关架构和需要用到的工具链
#! /bin/bash
JOBNUM=4
NPWD=`realpath .`
export ARCH=arm
export CROSS_COMPILE=$NPWD/../gcc/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-
if [ $1_x == cp_x ];then
echo $PWD
cp $NPWD/rootfs/build/linux-custom/arch/arm/boot/zImage ~/tftp
cp $NPWD/rootfs/build/linux-custom/arch/arm/boot/dts/sun8i-h3-orangepi-one.dtb ~/tftp
elif [ $1_x == busybox_x ];then
cd $NPWD/../busybox
echo $PWD
mkdir -p $NPWD/bboxbuild
if [ $2_x == install_x ];then
mkdir -p $NPWD/rootfs
make O=$NPWD/bboxbuild CONFIG_PREFIX=$NPWD/rootfs $2 -j$JOBNUM
else
if [ $3_x == _x ];then
make O=$NPWD/bboxbuild $2 -j$JOBNUM
else
make O=$NPWD/bboxbuild $2 $3
fi
fi
else
echo "help"
fi
二、静态编译并测试busybox
1、配置busybox静态编译选项
关于配置busybox这里我们可以根据自己的需求配置相关的小工具,静态编译主要关注的选项是CONFIG_STATIC,对应于下图Build static binary (no shared libs),执行命令./build.sh busybox menuocnfig
2、编译busybox
执行./build.sh busybox
对busybox进行编译,编译完成后出现类似提示Static linking against glibc,这里是glibc还是uclibc,具体看工具链编译时指定的c库是什么
3、安装busybox
使用./build.sh busybox install
安装busybox到脚本目录下的rootfs目录,这是因为我们在脚本中指定了安装目录就是当前的rootfs目录,可以看到我们安装的目录rootfs,及其下方的
4、测试busybox
这里测试我们通过之前文章的nfs根文件的映射进行测试,由于我们之前创建的nfs文件系统时通过软连接的方式进行连接的,这里我们只需要把,软件重新制定到mkrootfs目录下的rootfs文件夹即可
ln -snf mkrootfs/rootfs nfs
把nfs连接重新连接到nfs根文件系统中,由于开发板的配置本来就是nfs根文件目录,我们这里只需要重启开发板就可以加载到nfs了
-
can't run '/etc/init.d/rcS': No such file or directory 和 can't open /dev/tty2: No such file or directory ,这里是因为busybox上电会先执寻找 /etc/init.d/rcS脚本,但是因为我们的根文件系统只有busybox编译出的东西,没有脚本也没有终端设备目录,我们可以先建立一个dev目录在rootfs中,然后重启开发板
image.png -
虽然还是提示can't run '/etc/init.d/rcS': No such file or directory,但是很明显,已经进入了一个可以使用的shell终端了,后续具体的配置在跟文件系统制作章节具体介绍,这里我们先研究动态编译busybox并加载测试
image.png
三、动态编译并测试busybox
1、配置、编译、安装busybox
1、关于配置busybox这里我们可以根据自己的需求配置相关的小工具,busybox的默认配置就是动态编译,只要把CONFIG_STATIC选项去掉即可,对应于Build static binary (no shared libs),执行命令./build.sh busybox menuocnfig
配置去掉CONFIG_STATIC选项
2、通过./build.sh busybox
编译动态的busybox,这里会出现Trying libraries:表面是动态连接到后面的库
3、移除之前静态的rootfs
sudo rm -r rootfs
4、通过
./build.sh busybox install
安装动态的busybox到rootfs
5、在rootfs创建dev目录
mkdir -p rootfs/dev
2、测试busybox
这里的测试方法和前面介绍的动态busybox一样,我们先重启开发板
-
Kernel panic - not syncing: No working init found,这里是因为我们使用了动态连接,但是没有把相应的动态库放入根文件系统,导致busybox运行时没有对应的库,同时我们可以看到,如果我们在没有指定init的情况下,内核会自动的按照/sbin/init、/etc/init、/bin/init、/bin/sh顺序去查找初始程序并运行
image.png - 加载动态库
- 通过readelf查找busybox的动态库
arm-linux-gnueabihf-readelf -d rootfs/bin/busybox | grep NEEDED
vencol@pcvencol:~/code/self$ arm-linux-gnueabihf-readelf -d rootfs/bin/busybox | grep NEEDED
0x00000001 (NEEDED) Shared library: [libm.so.6]
0x00000001 (NEEDED) Shared library: [libresolv.so.2]
0x00000001 (NEEDED) Shared library: [libc.so.6]
2.通过objdump查找busybox的动态库
arm-linux-gnueabihf-objdump -x rootfs/bin/busybox | grep NEEDED
vencol@pcvencol:~/code/self$ arm-linux-gnueabihf-objdump -x rootfs/bin/busybox | grep NEEDED
NEEDED libm.so.6
NEEDED libresolv.so.2
NEEDED libc.so.6
3.通过strings 查找busybox的动态库
strings busybox | grep ^lib
vencol@pcvencol:~/code/self$ strings rootfs/bin/busybox | grep ^lib
libm.so.6
libresolv.so.2
libc.so.6
lib32
lib64
4.通过ldd查找,但是这里交叉工具链中并没有提供ldd工具,需要在开发板上执行,因为主机架构一般和开发板不同,所以才要交叉编译
最后我们可以确定动态库如下,这里需要注意的是ld-linux-armhf.so.3和ld-2.25.so,因为这两个库是执行时连接需要用到的库,无论是从readlf、objdump还是string都没有体现出来,很多小伙伴,也是因为缺少工具链的这里动态连接库,导致动态连接的时候失败,这个只能通过ldd才能体现出来。但是几乎所有通过交叉工具链动态编译的软件,都会依赖这两个库,所以,如果不确定的时候就把他们都加到根文件的lib目录吧。一般对应的库文件都会出现在交叉工具链了目录下的arm-linux-gnueabihf/libc/lib路径下
vencol@pcvencol:~/code/self$ ll rootfs/lib/
total 20816
drwxrwxr-x 2 vencol vencol 4096 Aug 1 05:55 ./
drwxrwxr-x 7 vencol vencol 4096 Aug 1 05:56 ../
-rwxr-xr-x 1 vencol vencol 1111436 Aug 1 05:55 ld-2.25.so*
lrwxrwxrwx 1 vencol vencol 10 Aug 1 05:55 ld-linux-armhf.so.3 -> ld-2.25.so*
-rwxr-xr-x 1 vencol vencol 13877292 Aug 1 05:55 libc-2.25.so*
lrwxrwxrwx 1 vencol vencol 12 Aug 1 05:55 libc.so.6 -> libc-2.25.so*
-rwxr-xr-x 1 vencol vencol 5948304 Aug 1 05:55 libm-2.25.so*
lrwxrwxrwx 1 vencol vencol 12 Aug 1 05:55 libm.so.6 -> libm-2.25.so*
-rwxr-xr-x 1 vencol vencol 359708 Aug 1 05:55 libresolv-2.25.so*
lrwxrwxrwx 1 vencol vencol 17 Aug 1 05:55 libresolv.so.2 -> libresolv-2.25.so*
添加了对应库后,一般就可以成功加载了,但是返现动态库加载的根文件要比静态链接的大的多,这个是因为现在的软件只有busybox,后面工具软件多了,肯定会使用动态连接,可以根据需要适当做精简
四、构建根文件系统
1、根文件系统的目录
这里目录bin、linuxrc、sbin、usr是我们用busybox生成的目录,我们主要需要修改的目录有dev、etc和lib,lib主要是需要拷贝库文件,这里主要展开需要修改的文件
├── bin
├── dev
│ ├── console
│ ├── null
│ └── zero
├── etc
│ ├── fstab
│ ├── group
│ ├── hostname
│ ├── init.d
│ ├── inittab
│ ├── network
│ ├── passwd
│ ├── passwd-
│ └── profile
├── lib
├── linuxrc -> bin/busybox
├── mnt
├── proc
├── root
├── sbin
├── sys
├── tmp
├── usr
└── var
2、具体创建步骤
- 进入根文件系统目录,创建文件系统的顶层目录
cd rootfs && mkdir -p dev etc mnt proc var tmp sys root lib
- 创建dev目录相关设备
mknod dev/null c 1 3
mknod dev/zero c 1 5
mknod dev/console c 5 1
- 设置etc目录相关配置
1.创建etc/inittab文件,该文件是系统启动后,按照上面的描述进行启动的配置文件
::sysinit:/etc/init.d/rcS #系统启动脚本
::ctrlaltdel:/sbin/reboot #组合键Ctrl+Alt+Del组合键,重启系统
::shutdown:/bin/umount -a -r #关机前umount所有挂载
ttyS0::askfirst:-/bin/sh #启动串口终端,如果需要登录改为/bin/login
::shutdown:/etc/init.d/rcK #系统关机脚本
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r #关机前umount所有挂载
- 创建 etc/init.d目录,并新建文件etc/init.d/rcS,并给文件执行权限
sudo chmod a+x etc/init.d/rcS
mount -a #首先挂载所有在fstab定义的内容
mkdir /dev/pts
mount -t devpts devpts /dev/pts
/bin/hostname -F /etc/hostname
ifconfig usb0 up 192.168.137.2
route add default gw 192.168.137.1 usb0
- 创建 etc/init.d目录,并新建文件etc/init.d/rcK,并给文件执行权限
sudo chmod a+x etc/init.d/rcK
ifconfig usb0 down
- 新建etc/fstab文件
proc /proc proc defaults 0 0
sysfs /sys sysfs defaults 0 0
mdev /dev tmpfs defaults 0 0
none /tmp tmpfs defaults 0 0
none /var tmpfs defaults 0 0
- 新建etc/profile,其中的PS1参数可以参考网站做相应修改https://blog.csdn.net/litao31415/article/details/50188243
USER="`id -un`"
LOGNAME=$USER #登录之后使用用户名显示
HOSTNAME="rootfs_by_vencol" #主机名
PS1="[\u@\h \w]# " #终端显示信息
if [ ! -z ${SSH_TTY} ]; then
export PATH=/sbin:/usr/sbin:/bin:/usr/bin
fi
- 设置lib目录
lib目录主要是库文件和模块文件的所在目录,一般模块文件可以通过源码make modules_install安装到lib目录,这样就可以不用拷贝模块文件了,所以我们主要介绍的是拷贝libc,libc一般在工具链目录下有,比如我这里的工具链是arm-linux-gnueabihf。
cp -a arm-linux-gnueabihf/arm-linux-gnueabihf/libc/lib/ rootfs/lib
rm -f rootfs/lib/*.a
#去除符号表,节省空间,但是不能反汇编了
#${COMPILE_DIR}arm-none-linux-gnueabi-strip rootfs/lib/*
- 拷贝虚拟机上的/etc/group和/etc/passwd到rootfs/etc
修改passwd为root:FMKTwEUCSZm9Q:0:0:root:/root:/bin/sh,即只保存与root相关项,而且最后改成/bin/sh
修改group为root:x:0:root
这样登录的时候就是用和虚拟机root用户一样的密码登录,如果不行,可以先修改/etc/init.d/rcS文件,开机直接进入shell,在shell里面修改root用户密码,之后再修改/etc/init.d/rcS文件为开机login。
3、写成根文件系统创建脚本
#! /bin/bash
JOBNUM=4
NPWD=`realpath .`
#mkdir -p roottemp
cd rootfs
mkdir -p dev etc mnt proc var tmp sys root lib
sudo mknod dev/null c 1 3
sudo mknod dev/zero c 1 5
sudo mknod dev/console c 5 1
cat << EOF > etc/inittab
::sysinit:/etc/init.d/rcS #系统启动脚本
::ctrlaltdel:/sbin/reboot #组合键Ctrl+Alt+Del组合键,重启系统
ttyS0::respawn:-/bin/login #启动串口终端,如果需要登录改为/bin/login
#ttyS0::askfirst:-/bin/login #启动串口终端,如果需要登录改为/bin/login
#ttyS0::askfirst:-/bin/sh #启动串口终端,如果需要登录改为/bin/login
::shutdown:/etc/init.d/rcK #系统关机脚本
#::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r #关机前umount所有挂载
EOF
mkdir -p etc/init.d
cat << EOF > etc/init.d/rcS
# !/bin/sh
mount -a #首先挂载所有在fstab定义的内容
mkdir /dev/pts
mount -t devpts devpts /dev/pts
/bin/hostname -F /etc/hostname
ifconfig usb0 up 192.168.137.2
route add default gw 192.168.137.1 usb0
EOF
sudo chmod +x etc/init.d/rcS
cat << EOF > etc/init.d/rcK
# !/bin/sh
ifconfig usb0 down
EOF
cat << EOF > etc/fstab
proc /proc proc defaults 0 0
sysfs /sys sysfs defaults 0 0
mdev /dev tmpfs defaults 0 0
none /tmp tmpfs defaults 0 0
none /var tmpfs defaults 0 0
EOF
mkdir -p etc/network
cat << EOF > etc/network/interfaces
# interface file auto-generated by buildroot
auto lo
iface lo inet loopback
#auto eth0
#iface eth0 inet dhcp
# pre-up /etc/network/nfs_check
# wait-delay 15
# hostname $(hostname)
auto usb0
iface usb0 inet static
pre-up /etc/network/nfs_check
address 192.168.137.2
netmask 255.255.255.0
gateway 192.168.137.1
#dns-nameservers 8.8.8.8 192.168.137.1 211.136.20.203
EOF
cat << EOF > etc/resolv.conf
nameserver 192.168.137.1
nameseverr 8.8.8.8
nameseverr 10.8.16.30
EOF
cat << EOF > etc/hostname
vencolfs
EOF
#cp -a $NPWD/../gcc/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/lib/ lib
#rm -f lib/*.a
#去除符号表,节省空间,但是不能反汇编了
#${COMPILE_DIR}arm-none-linux-gnueabi-strip rootfs/lib/*
#下面的是用户相关的信息,自行修改
cat << EOF > etc/passwd
root:FMKTwEUCSZm9Q:0:0:root:/root:/bin/sh
EOF
cat << EOF > etc/group
root:x:0:
EOF
#cat << EOF > etc/shadow
#root:$6$B7gzTyFF$Zm2fC5EQYdqupH.BwccJv0YX4XingPOqsMcu1vlWC4AaKof4ycDGlXooMs2m5ZxfDPvjhDicnkt/PuGBDqZtD1:18316:0:99999:7:::
#EOF
cat << EOF > etc/profile
USER="`id -un`"
LOGNAME=$USER #登录之后使用用户名显示
HOSTNAME="rootfs_by_vencol" #主机名
#HOSTNAME='/bin/hostname'
PS1="[\u@\h \w]# " #终端显示信息
if [ ! -z ${SSH_TTY} ]; then
export PATH=/sbin:/usr/sbin:/bin:/usr/bin
fi
EOF
五、无法解析域名
用了一段时间后发现,可以ping通ip但是无法DNS,经过一番折腾后发现,是因为没有支持DNS,因为编译出来的nslookup是可以解析域名的,但是直接ping却是不行的
这里找到了两个方案
- 安装文章https://www.cnblogs.com/liangwode/p/5584099.html中说的,把busybox编译成动态连接,同时添加相应的库文件,其中DNS需要用到的文件有:/lib/libnss_dns.so.2、/lib/libnss_files.so.2、/lib/libresolv.so.2、/etc/resolv.conf、/etc/nsswitch.conf
- https://www.dazhuanlan.com/2020/01/06/5e12988c64495/这个文章中,提到添加route gateway,并把ip添加到/etc/resolv.conf文件中,但是实际使用失败了,后来发现https://blog.csdn.net/u013625451/article/details/79007441这文章中提到了,busybox官网的说法是需要动态库
- 后来在下面的网址发现了解决方案,其思路是,在ip连接前,先自己模拟dns的协议,解析/etc/resolv.conf并向里面的服务器发送数据进行dns解析。https://blog.csdn.net/bingyu9875/article/details/104684985
1、把下面的文件内容,添加到busybox源码的libbb/xconnect.c文件的开头
2、修改该文件中str2sockaddr函数中getaddrinfo改为hgetaddrinfo。
#include
#include
#include
#include
#include
#define oops(msg) { perror(msg); exit(1);}
#include
#include
#include
#include
void fillip(char* buffer, const char* ip);
char* name2ip(const char* name);
/**
* * head len: 12
* * query: ? + 4
* * total: strlen(query.buffer) + 16
* */
typedef struct{
u_short txid;
u_short flag;
u_short question;
u_short answer;
u_short authority;
u_short additional;
char buffer[256];
struct {
u_short type;
u_short cls;
}query;
}dns_req;
/** length of resp: 12 */
typedef struct{
u_short txid;
u_short flag;
u_short question;
u_short answer;
u_short authority;
u_short addtional;
char buffer[1024];
struct{
u_short name;
u_short type;
u_short cls;
u_short live_l;
u_short live_h;
u_short len;
struct in_addr addr;
}resp;
}dns_res;
int hgetaddrinfo(const char *node, const char *service,
const struct addrinfo *hints,
struct addrinfo **res){
/* int rc = getaddrinfo(node,service,hints,res);
* if(rc == 0)//todo: != -> ==
* return rc;
* */
char *ip = name2ip(node); /// 如果 ping www.baidu.com
return getaddrinfo(ip,service,hints,res); /// 那么此时 node 指向字符串 "www.baidu.com"
}
char *get_dns_name()
{
/*
static char buf[8]="8.8.8.8";
return buf;
*/
int fd;
static char buf[1024],*pstr,*pdns;
fd = open("/etc/resolv.conf", O_RDWR);
read(fd,buf,1020);
pstr = strstr(buf,"nameserver");
pstr += strlen("nameserver");
while(!isdigit(*pstr)){
pstr ++;
}
pdns = pstr;
while(isdigit(*pstr) || (*pstr == '.') ){
pstr ++;
}
*pstr = '\0';
close(fd);
return pdns;
}
char * name2ip(const char *node){
struct sockaddr_in dns;
dns_req req;
dns_res res;
int sockid, len, index;
get_dns_name();
char dnsip[32];
strcpy(dnsip,get_dns_name());
sockid = socket(PF_INET, SOCK_DGRAM, 0);
if(sockid == -1)
oops("socket");
memset((void*)&dns, 0, sizeof(dns));
dns.sin_family = AF_INET;
dns.sin_port = htons(53);
dns.sin_addr.s_addr = inet_addr(dnsip);
memset((void*)&req, 0, sizeof(req));
req.txid = htons(0x4419);
req.flag = htons(0x0100);
req.question = htons(1);
fillip(req.buffer,node);
req.query.type=htons(1);
req.query.cls=htons(1);
memcpy(req.buffer+strlen(req.buffer)+1, (void*)(&req.query), sizeof(req.query));
sendto(sockid, (void*)&req, strlen(req.buffer)+17, 0,
(struct sockaddr*)&dns, sizeof(dns));
recvfrom(sockid, (void*)&res, sizeof(res), 0,
(struct sockaddr*)&dns, &len);
index = strlen(res.buffer)+5;
while(1){
memcpy((void*)&(res.resp), res.buffer+index, 12);
if(ntohs(res.resp.type)==1){
memcpy((void*)&(res.resp.addr), res.buffer+index+12, 4);
break;
}
index += ntohs(res.resp.len) + 12;
}
return inet_ntoa(res.resp.addr);
}
void fillip(char* buffer, const char* ip){
int i,j=0;
for(i = 0; ip[i] != 0; i ++){
if(ip[i] != '.'){
buffer[i+1] = ip[i];
}
else{
buffer[j] = i - j;
j = i + 1;
}
}
buffer[j] = i - j;
}