LFS的全称是Linux From Scratch,从字面意思就可以明白这是一个从头(源代码)开始制作Linux系统的过程。
有一个开源项目,专门用来实现这个过程。
LFS项目的官方网址是http://www.linuxfromscratch.org/,从这里可以了解到从头完成一个Linux系统的所有内容。
本文就是在这个基础上,来尝试编译自己的Linux系统。
(后面的例子中配置的图片中的用户或者时间之类的可能上下有出入,原因是因为是在不同的时间和机器上编译的,但是具体内容应该是正确的)
首先LFS的过程被制作成了PDF文档,可以在http://www.linuxfromscratch.org/lfs/downloads/8.0/下载到,目前最新稳定版本是8.0版本。
以上这个是英文版本的,也有中文版本:https://linux.cn/lfs/LFS-BOOK-7.7-systemd/index.html,不过相比之下版本会有落后。
本文主要以8.0为基础。
另外还需要准备编译环境,这里使用的是如下的版本:
Linux home 4.4.0-83-generic #106~14.04.1-Ubuntu SMP Mon Jun 26 18:10:19 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
目前先以这个版本来实现,不过也不确定是否会有问题。
事实上LFS文档中说明了两个用来检测系统的脚本:version-check.sh
#!/bin/bash
# Simple script to list version numbers of critical development tools
export LC_ALL=C
bash --version | head -n1 | cut -d" " -f2-4
MYSH=$(readlink -f /bin/sh)
echo "/bin/sh -> $MYSH"
echo $MYSH | grep -q bash || echo "ERROR: /bin/sh does not point to bash"
unset MYSH
echo -n "Binutils: "; ld --version | head -n1 | cut -d" " -f3-
bison --version | head -n1
if [ -h /usr/bin/yacc ]; then
echo "/usr/bin/yacc -> `readlink -f /usr/bin/yacc`";
elif [ -x /usr/bin/yacc ]; then
echo yacc is `/usr/bin/yacc --version | head -n1`
else
echo "yacc not found"
fi
bzip2 --version 2>&1 < /dev/null | head -n1 | cut -d" " -f1,6-
echo -n "Coreutils: "; chown --version | head -n1 | cut -d")" -f2
diff --version | head -n1
find --version | head -n1
gawk --version | head -n1
if [ -h /usr/bin/awk ]; then
echo "/usr/bin/awk -> `readlink -f /usr/bin/awk`";
elif [ -x /usr/bin/awk ]; then
echo awk is `/usr/bin/awk --version | head -n1`
else
echo "awk not found"
fi
gcc --version | head -n1
g++ --version | head -n1
ldd --version | head -n1 | cut -d" " -f2-
grep --version | head -n1
gzip --version | head -n1
cat /proc/version
m4 --version | head -n1
make --version | head -n1
patch --version | head -n1
echo Perl `perl -V:version`
sed --version | head -n1
tar --version | head -n1
makeinfo --version | head -n1
xz --version | head -n1
echo 'int main(){}' > dummy.c && g++ -o dummy dummy.c
if [ -x dummy ]
then echo "g++ compilation OK";
else echo "g++ compilation failed"; fi
rm -f dummy.c dummy
以及library-check.sh:
#!/bin/bash
for lib in lib{gmp,mpfr,mpc}.la; do
echo $lib: $(if find /usr/lib* -name $lib|
grep -q $lib;then :;else echo not;fi) found
done
unset lib
第一个是用来检测LFS时需要用到的工具及其版本,这边使用的Ubuntu系统会发现有不少工具还不存在,下载即可。
目前调用version-check.sh脚本时还是会发现存在版本不匹配的问题,这个不知道是否会影响后续的操作,这里先存疑。
另外的一个问题就是Ubuntu默认的/bin/sh指向的是dash,这里需要改成bash。
修改的方法是使用dpkg-reconfigure dash命令,然后选择否即可,如下图所示:
这里需要注意上述命令需要用root用户来完成。
第二个脚本的用处是判断某三个库是否存在,必须要求它们都存在或都不存在。
下一步的准备工作就是下载各种源代码,包括Linux的源代码以及其它工具的源代码。
LFS的官网提供了工具的源代码下载,这样会省很多力气,具体的网址是:ftp://ftp.lfs-matrix.net/pub/lfs/lfs-packages/。
这里其实有个坑,在LFS文档中列出了所有的Package,并且给了下载的地址,但是实际上真正一个个去下载的话非常麻烦,而且有些不一定能够下载成功。
所以最好还是从上述的FTP中下载,下载得到的文件:
再之后的一步操作是设置环境变量LFS,这个后续编译的时候会一直用到,所以这里将它写入到root和用户(后续会创建一个特定的用户lfs)的.bashrc文件中,这样就不需要每次都设置了:
export LFS=/mnt/lfs
这里将LFS设置成/mnt/lfs(注意需要在/mnt下面创建lfs目录),然后/mnt/lfs又会是外接硬盘的挂载点。
这里使用了一个ext4格式的硬盘作为外接硬盘,后续的编译都在这个硬盘里面完成。
为了能够使用硬盘,每次都需要挂载该该硬盘,下面是使用的命令:
sudo mount -v -t ext4 /dev/sdc2 $LFS
这里需要使用root权限来挂载,所以有sudo。
关于硬盘的分区和格式化,可以使用fdisk和mkfs命令,这里不再多说。
在$LFS下会创建几个目录:
1. sources,用来存放前面下载到的源代码;
2. tools,用来存放编译得到的临时程序,它们不是LFS的一部分,之后为tools创建软链接:
这里ln命令的作用就是创建链接,-s参数表示创建的是软链接,-v用来显示命令执行的信息。
$LFS/tools是源目录(文件),/是目标目录(文件),不过并不是/直接链接到$LFS,而是会在/下创建tools目录,如下所示:
再之后是创建一个新用户lfs,它就是为了LFS而存在的,下面是具体的命令:
到最后的命令会切换到了lfs这个用户上。
之后为lfs创建两个文件,.bash_profile:
exec env -i HOME=$HOME TERM=$TERM PS1='\u:\w\$ ' /bin/bash
和.bashrc:
set +h
umask 022
LFS=/mnt/lfs
LC_ALL=POSIX
LFS_TGT=$(uname -m)-lfs-linux-gnu
PATH=/tools/bin:/bin:/usr/bin
export LFS LC_ALL LFS_TGT PATH
然后执行如下的命令:
source ~/.bash_profile
执行.bash_profile脚本会创建一个干净的Shell环境,里面只有在脚本中提到的HOME,TERM等几个环境变量,它会接着执行.bashrc文件,又引入了几个环境变量;
以上就是所有的准备工作。
需要注意的是,因为不可能一次完成整个LFS,因此当工作中断之后要重新开始,需要注意几点:
1. 要重新mount硬盘;(需要在root下挂载硬盘之后再进入lfs用户)
2. 要重新进入lfs用户,并执行source ~/.bash_profile(更新,这个步骤每次进入lfs时会自动完成);
3. 下面的编译都要使用lfs用户来进行;
到目前为止,挂载硬盘的目录如下:
完成准备工作之后就到了下一步工作。这里还不是直接编译我们需要的Linux,而是创建一个小系统。
这个小系统包含两个部分,一部分是与当前系统无关的工具链,另一部分是用上述工具链创建的其它工具。
本节内容中创建的工具会被放在之前提到的$LFS/tools目录下。
注意本节中的操作必须都有lfs来完成。
在正式开始之前,LFS文档中说明了一些系统无关工具链需要注意的点。
首先,有一个目标平台的概念,通过./config.guess可以查看:
为了保证工具不依赖于平台,这里使用了一个小技巧,在configure的时候会通过--target=来指定目标平台,而这个平台是我们自定义的LFS_TGT,它在前面已经定义过了,在这里就是x86_64-lfs-linux-gnu,这里的目的就是要于当前主机平台不一致。
其次,要保证ld能够正确链接(我们编译binutil的汇编器和连接器会被保存在/tools/bin和/tools/$LFS_TGT/bin下面,这个/tools在前面已经链接到了$LFS/tools),可以通过ld --verbose | grep SEARCH来查看。
这是现在的结果:
后续真正使用的时候就不应该是这个结果了。
下面开始编译工具链。
第一个需要编译的工具是binutil。
首先在$LFS目录下创建build目录(这里需要先使用root用户执行了chown命令,修改整个$LFS的所有人为lfs),之后所有的编译都放在它下面:
然后将之前下载到的binutil源代码解压到build目录下:
注意,对于Binutil需要创建额外的目录来编译,这里就在binutil-2.27目录下创建build目录(第二个build目录了,跟前一个不要混淆):
然后在build目录执行如下的命令:
../configure --prefix=/tools --with-sysroot=$LFS --with-lib-path=/tools/lib --target=$LFS_TGT --disable-nls --disable-werror
这里的参数说明:
--prefix用来指定Binutil二进制安装的目录,这里其实就是$LFS/tools,因为/tools被链接到了这里;
--with-sysroot是为了交叉编译,这里告诉编译系统从$LFS里面找目标系统库(this tells the build system to look in $LFS for the target system libraries as needed);
--with-lib-path告诉链接器从/tools/lib目录下找库;
--target指定为$LFS_TGT(于config.guess的不一样)就是为了告诉编译系统我们在编译交叉链接器;
结果如下(注意这里显示没有打印完全):
之后运行make,下面是结果:
在安装Binutil之前,对于x86_64的系统来说,还要运行以下的脚本。
case $(uname -m) in
x86_64) mkdir -v /tools/lib && ln -sv lib /tools/lib64 ;;
esac
这里首先创建了一个/tools/lib目录,因为Binutil编译得到的库需要放到里面去。
另外对于x86_64位系统还需要有一个lib64的目录,它直接链接到了lib上:
最后就是make install:
完成之后可以在/tools目录下看到安装的工具:
其中bin目录下是各种工具二进制:
lib和lib64目录是空的;
share目录下是一个帮助文档;
x86_64-lfs-linux-gnu目录(也就是$LFS_TGT)下也有一个bin和一个lib目录,bin目录下是Binutil工具:
它们应该跟/tools/bin下面的工具是一样的。
lib目录下是链接器脚本:
执行/tools/bin目录下的ld:
可以看到搜索路径是$LFS/tools下面的了。
不过这个还不是全局的ld,怎么把它变成全局的ld,或者以后如何使用这个ld,目前还不知道。
第二个需要编译的是GCC。
首先将GCC的源代码放到build目录,然后还需要将三个目录放到GCC编译目录下,分别是mpfr、gmp和mpc。
以下是所有需要的文件:
注意gmp、mpc和mpr是后来加入的,使用的命令如下:
tar -xf ../../sources/mpc-1.0.3.tar.gz
mv -v mpc-1.0.3.tar.gz mpc
其他两个目录也类似。
当所有的三个目录都加入后,需要touch一下它们,否则之后的编译会报错。
另外,还需要安装automake、flex等工具。
这里需要严格按照文档的说明来编译:
1. 首先压缩gcc源代码到一个目录,这里就是$LFS/build/目录下,进入gcc源代码目录;
2. 再压缩mpfr、mpc和gmp到gcc目录下,并重命名成不带版本号的目录;
3. 在gcc源代码目录下,运行如下的两端脚本,一是gcc_pass1_1.sh:
#!/bin/bash
for file in gcc/config/{linux,i386/linux{,64}}.h
do
cp -uv $file{,.orig}
sed -e 's@/lib\(64\)\?\(32\)\?/ld@/tools&@g' \
-e 's@/usr@/tools@g' $file.orig > $file
echo '
#undef STANDARD_STARTFILE_PREFIX_1
#undef STANDARD_STARTFILE_PREFIX_2
#define STANDARD_STARTFILE_PREFIX_1 "/tools/lib/"
#define STANDARD_STARTFILE_PREFIX_2 ""' >> $file
touch $file.orig
done
注意这里需要将上述代码放到一个sh脚本中然后执行,不要直接在shell中逐行输入执行,否则可能会导致问题。
上面的脚本因为显示的问题会有()没有显示出来,就是sed那一行,实际应该是这样的:
这里的作用主要是修改gcc/config下面的头文件,修改其中的动态库的位置,比如linux.h文件的修改如下:
其它的文件修改这里不再多说。
另外对于x86_64位平台,需要运行另一个脚本gcc_pass1_2.sh:
#!/bin/bash
case $(uname -m) in
x86_64)
sed -e '/m64=/s/lib64/lib/' \
-i.orig gcc/config/i386/t-linux64
;;
esac
上面的代码也要放到脚本里面然后执行。修改的文件如下:
执行玩上述的脚本之后,再创建独立的build目录,并进入该目录。
之后运行configure命令:
../configure --target=$LFS_TGT --prefix=/tools --with-glibc-version=2.11 --with-sysroot=$LFS --with-newlib --without-headers --with-local-prefix=/tools --with-native-system-header-dir=/tools/include --disable-nls --disable-shared --disable-multilib --disable-decimal-float --disable-threads --disable-libatomic --disable-libgomp --disable-libmpx --disable-libquadmath --disable-libssp --disable-libvtv --disable-libstdcxx --enable-languages=c,c++
configure之后再make进行编译:
再之后运行make install来安装,安装的GCC位于tools目录下面:
这里在bin目录下又多了一些工具,是跟GCC工具相关的:
这里还增加了一个include目录,不过目前是空的,会在下一节操作中安装;
GCC安装之后,lib目录下也有新的内容了(lib64是lib的链接,所以内容一样):
libexec是安装GCC之后多出来的目录,其内容如下:
share里面的文档也会增加GCC相关的文档;
x86_64-lfs-linux-gnu目录下没有增加东西。
以上是GCC的第一个次编译,后面还有第二次编译。
这个步骤只是提供了一些API头文件,后续Glibc的编译需要使用到。
具体的步骤如下:
1. 解压linux-4.9.9.tar.xz文件,并进入到该目录;
2. 运行如下的命令:
make mrproper
用来清理老文件,不过对于我们新解压的其实不是很必要;
3. 再运行如下的命令:
make INSTALL_HDR_PATH=dest headers_install
这里在当前目录下创建了dest目录,并安装头文件给到其下的include目录:
4. 之后将安装的头文件放到$LFS/tools中:
cp -rv dest/include/* /tools/include
之后可以在$LFS/tools/include中看到头文件:
下面是C库的编译,步骤如下:
1. 解压glibc文件,这里需要注意打patch:
2. 在Glibc目录下创建build目录,我们需要在单独的目录下进行编译:
3. 之后是configure:
../configure --prefix=/tools --host=$LFS_TGT --build=$(../scripts/config.guess) --enable-kernel=2.6.32 --with-headers=/tools/include libc_cv_forced_unwind=yes libc_cv_c_cleanup=yes
--host和--build一起使用使编译系统使用交叉的编译器和链接器,就是我们之前创建的那些GCC和LD之类的工具,可以看下面的例子:
--with-headers指定了编译时使用的头文件是前一节安装的头文件;
4. 之后就是make和make install,结果如下:
以上就是编译的结果。
安装完Glibc之后的目录:
etc目录是新多出来的,里面有一个rpc文件,作用不明;
include目录下Glibc安装之后会增加很多的文件;
lib和lib64里面同样会增加很多的文件;
libexec目录下面多了一个getconf目录,里面的文件如下:
sbin是新增的目录:
share目录下同样会增加很多的目录和文件;
var目录是Glibc安装之后新增的:
x86_64-lfs-linux-gnu目录下没有变化;
下面有一个测试:
1. 首先创建一个源文件,就一行最简单的代码:
int main(){}
2. 然后使用之前编译得到的工具进行编译:
得到以上的结果说明之前的操作都是正确的。(以上是64位机器的结果)
如果这里的动态库不是/tools下面的,很有可能是前面gcc编译前执行的脚本有问题,需要再看一下。
下面是编译C++库,步骤如下:
1. 解压源文件,注意它是GCC源文件的一部分,所以这里只要压缩GCC源文件就可以了,其它的mpfr/mpc/gmp最好也解压了;
2. 创建build目录并进入build目录;
3. configure如下:
../libstdc++-v3/configure --host=$LFS_TGT --prefix=/tools --disable-multilib --disable-nls --disable-libstdcxx-threads --disable-libstdcxx-pch --with-gxx-include-dir=/tools/$LFS_TGT/include/c++/6.3.0
这里的--host指定了$LFS_TGT,所以这里最终会使用的编译是x86_64-lfs-linux-gnu-g++等;
4. 之后就是make,结果如下:
完成后make install。
然后会在/mnt/lfs/tools/x86_64-lfs-linux-gnu目录下产生include目录:
也就是说这里的编译目的就是为了产生这个incldue文件。
具体的操作跟第一次一样,只不过有不同的配置。下面是具体的流程:
1. 解压压缩包,并进入压缩后的目录;
2. 创建build目录并进入;
3. 宏设置和configure:
CC=$LFS_TGT-gcc AR=$LFS_TGT-ar RANLIB=$LFS_TGT-ranlib ../configure --prefix=/tools --disable-nls --disable-werror --with-lib-path=/tools/lib --with-sysroot
注意这里的宏设置和configure必须在同一行,否则就是无效的。
4. make和make install;
5.调整链接器:
make -C ld clean
make -C ld LIB_PATH=/usr/lib:/lib
cp -v ld/ld-new /tools/bin
这里到底哪些目录被修改已经不是很好看了,这里不说明了。
需要说明的是这里会创建x86_64-pc-linux-gnu目录,并其bin目录下放置生成的工具。
下面是GCC的第二次编译,基本操作如下:
1. 之前的解压和进入压缩目录等操作都不变,脚本执行gcc_pass1_1.sh和gcc_pass1_2.sh(见第一次GCC编译)和build目录生成也不变;
2. 创建limits.h文件:
cat gcc/limitx.h gcc/glimits.h gcc/limity.h > `dirname $($LFS_TGT-gcc -print-libgcc-file-name)`/include-fixed/limits.h
3. configure:
CC=$LFS_TGT-gcc CXX=$LFS_TGT-g++ AR=$LFS_TGT-ar RANLIB=$LFS_TGT-ranlib ../configure --prefix=/tools --with-local-prefix=/tools --with-native-system-header-dir=/tools/include --enable-languages=c,c++ --disable-libstdcxx-pch --disable-multilib --disable-bootstrap --disable-libgomp
4. make以及make install;
5. 创建软链接:
ln -sv gcc /tools/bin/cc
到目前/tools目录的内容:
下面有一个测试。
1. 首先还是创建一个源文件,最简单的源文件就可以:
echo 'int main(){}' > dummy.c
2. 然后编译:
cc dummy.c
3. 然后读取编译后的文件:
readelf -l a.out | grep ': /tools'
结果如下:
这里就是要跟上次的测试结果一样。
但是感觉这里的测试这样还是不够的,因为gcc第二次编译的时候用的就是之前一次编译得到的gcc,因此这里编译生成的gcc本身也需要测试链接情况:
如上图所示,要保证gcc本身也符合要求。
到这里最主要的工具就完成编译了。
后面还有不少其它的工具,如下:
1. Tcl——编译过程见文档,编译完成之后可以直接使用了:
2. Expect——编译过程见文档,这里make test的时候会报错,不过似乎没有什么问题,可以正常安装,安装后如下:
3. DejaGNU——编译过程见文档,编译后得到的工具是runtest:
4. Check——编译过程见文档,这里主要是make check的时候需要的时间比较长;编译后得到的工具是checkmk:
5. Ncurses——编译过程见文档;
6. Bash——编译过程见文档;这里需要打patch:
这里安装了一个bash。
7. Bison——编译过程见文档;
8. Bzip——编译过程见文档;这里需要patch,且不需要configure。
9. Coreutils——编译过程见文档;这里需要打patch。
编译的过程中有报错:
这个错误比较熟悉,之前编译GCC时也有类似的错误。
这里需要make distclean清除中间文件,而后touch一下所有的文件。
之后在configure和make。
10. Diffutils——编译过程见文档;
11. File——编译过程见文档;
12. Findutils——编译过程见文档;
这里make check的时候出现了一次很长时间的停顿,原因不明;不过重试之后还是没有问题。
13. Gawk——编译过程见文档;
这里make check的时候有报错,不过可以正常安装;
14. Gettext——编译过程见文档;
15. Grep——编译过程见文档;
16. Gzip——编译过程见文档;
17. M4——编译过程见文档;
18. Make——编译过程见文档;
19. Patch——编译过程见文档;
20. Perl——编译过程见文档;
这里的编译方式稍微有点不一样,不过按照文档说明编译即可。
不过编译过程中报错了:
目前原因不明......
从打印来看问题处在librt.so上,之前编译的时候已经生成这个文件了:
librt是Glibc的一部分,是在Glibc编译之后安装的。
通过nm -D librt-2.25.so这个命令查看库文件,可以看到有上面报错的那些个函数:
另外它为什么还是引用到了HOST下面的东西:/lib/x86_64-linux-gnu?
应该是前面哪里编译出错了,重新执行前面所有的操作之后,这个问题就消失了。
21. Sed——编译过程见文档;
22. Tar——编译过程见文档;
23. Texinfo——编译过程见文档;
24. Util-linux——编译过程见文档;
编译的时候也报错了:
需要确定/tools/include/bits下的resource.sh是哪个安装的,为什么会不一样?
经过确定也是Glibc安装的。看来之前Glibc编译和安装还是有问题啊。
25. Xz——编译过程见文档;
注意,上述的编译中会有个make check,这里需要安装autogen才能正常使用。
到这里第一部分的编译到此结束,这里还有两点需要做:
1. 如果空间有限,可以对之前生成的工具进行strip,不过这边空间够大,不做这步;
2. 将工具的权限改成root:
chown -R root:root $LFS/tools
注意这个动作需要root用户来做。
以上是本文的所有内容。
之后的内容会在LFS-Linux From Scratch学习笔记——LFS系统编译里面。