Learn from Linux from Scratch (LFS) (Part 2)

Part 1中,涉及的知识点主要集中在宿主机上准备构建环境。本篇将记录对应于书中第5章”Constructing a Temporary System”一章的从“Introduction”到“GCC-9.2.0 – Pass 2”有关内容。

为了方便阅读,几个主要的变量说明如下

LFS=/mnt/lfs
LC_ALL=POSIX
LFS_TGT=$(uname -m)-lfs-linux-gnu
PATH=/tools/bin:/bin:/usr/bin

/tools为指向$LFS/tools的符号链接。

简易的编译用时参考:Standard Build Unit(SBU)

LFS定义编译Binutils的时长为1个SBU。显然SBU并不是一个学术上很严谨的计量方法,但却很方便的给出了编译用时的估计方法。

我的系统的SBU大约是3m12.622s。按照这个估计,我完成10 SBU的”GCC-9.2.0 – Pass 1″大约需要32分。为了稍微快一点我在构建GCC-9.2.0 – Pass 1时为make添加了-j2的选项,最后make完用时18m55.776s。

为了交叉编译而设置的configure脚本的选项

--prefix=/tools:make install到自定义位置时最常见的选项。因为我们希望所有中间工具链全部生成到/tools所以所有的configure都会有这个选项。

--with-sysroot=$LFS:告知编译系统从$LFS中查找所需要的库

--target=$LFS_TGT:设置目标的架构。虽然在个人PC上,$LFS_TGT无非就是x86或者x86_64这两种情况,但是LFS明显是考虑到了PowerPC、ARM这些奇奇怪怪的架构。

--with-lib-path=/tools/lib(Binutils pass 1),--with-headers=/tools/include(Glibc pass 1),--with-local-prefix=/tools--with-native-system-header-dir=/tools/include(GCC pass 1)

用readelf确定interpreter

现在我们要接触一些二进制工具,以及一些关于Linux可执行文件方面的事情。目前使用的readelf工具(来自Binutils)尚取自宿主机。但是这不妨碍我们讨论这个问题。

首先观察宿主机上的GCC编译一个“空壳”C程序:

echo 'int main(){}' > dummy.c
gcc dummy.c
readelf -l a.out | grep interpreter
# 以下为回显
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

readelf -l的作用是读取程序头(Program Header),根据Computer Science from the Bottom Up网站Dynamic Linking一章对interpreter的说明

ELF allows executables to specify an interpreter, which is a program that should be used to run the executable. The compiler and static linker set the interpreter of executables that rely on dynamic libraries to be the dynamic linker.

一言以蔽之,一个程序要运行所必须的interpreter的路径是以某种方式硬编码在这个程序里面的。而且这个路径还是个绝对路径,就是说你的系统必须提供这个interpreter,这个程序才能运行得起来。

如果可执行程序中的interpreter是/lib64/ld-linux-x86-64.so.2,说明那这个可执行程序仍然依赖宿主系统。这不符合构建一个独立的Linux操作系统的目标。

现在,或许可以对Part 1中提出的“创建中间工具链的目的”做一个进一步说明。也就是说,我们构建中间工具链的部分原因是,中间工具链中所有可执行程序的interpreter都是(比如说)/mylib64/ld-linux-x86-64.so.2这样引用非宿主系统上动态库的形式,从而在工具链上摆脱宿主系统的影响。

那么硬编码interpreter路径的某种方式到底究竟是什么呢?答案是GCC的源码。在5.5节”GCC-9.2.0 – Pass 1″(以及5.10节”GCC-9.2.0 – Pass 2″)中,我们按书中给出的脚本修改了gcc/config/{linux,i386/linux{,64}}.h。这几个头文件中硬编码/lib64/ld...的地方,都被替换成了/tools/lib64/ld...。脚本还替换了一些宏,虽然我暂时不太明白为什么是那些宏,但肯定是为了避免引用宿主系统上的东西。

在构建完成中间工具链的Glibc后,我们可以用中间工具链的GCC编译一个dummy.c了,看看它的interpreter:

$LFS_TGT-gcc dummy.c  # $LFS_TGT-gcc即/tools/bin/x86_64-lfs-linux-gnu-gcc
readelf -l a.out | grep interpreter
# 以下为回显
      [Requesting program interpreter: /tools/lib64/ld-linux-x86-64.so.2]

现在已经是/tools/lib64/ld-linux-x86-64.so.2,这个interpreter已经不是宿主系统提供的了,是我们最开始编译Binutils得到的。

Learn from Linux from Scratch (LFS) (Part 1)

Learn from Linux from Scratch (LFS)这个系列的文章是我在阅读Linux from Scratch 9.1过程中的笔记。我用的宿主系统为CentOS 8(运行于VituralBox虚拟机中,VirtualBox虚拟机的Host是Windows 10)。

本篇对应于书的Preface至第4章”Final Preparations”。

分区并格式化

使用gparted,gnome-disks为LFS准备一个专门的分区不是难事。我没有用过fdisk,这里一并学习用fdisk分区。

fdisk被设计用来进行“交互式”分区,也就是说从用户友好度上来说它和那些用GUI的软件应该差不多。虽然如此还是有值得记住的命令行参数

fdisk -l  # 列出分区

不过我个人认为fdisk -l配合lsblk更好。使用这些命令的目的是为了找到要操作哪个分区。因为真正开始操作是从这个命令开始:

fdisk /dev/sdb

几个重要的交互命令:

  • p: 显示当前分区表
  • n: 创建新分区
  • d: 删除分区
  • t和L: 选择分区类型(例如是Linux分区或是Swap分区?)
  • w: 写入更改
  • q: 退出

格式化的命令就很简单了。还是先用lsblk或者fdisk -l /dev/sdb确认一下要操作的分区(以免出错)

fdisk -l /dev/sdb
Disk /dev/sdb: 20 GiB, 21474836480 bytes, 41943040 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x18eb9bc6

Device Boot Start End Sectors Size Id Type
/dev/sdb1 2048 206847 204800 100M 83 Linux
/dev/sdb2 206848 21178367 20971520 10G 83 Linux
/dev/sdb3 21178368 37955583 16777216 8G 83 Linux
/dev/sdb4 37955584 41943039 3987456 1.9G 82 Linux swap / Solaris

然后mkfs.ext4, mkswap进行格式化。这两个命令带-L选项还可以设置卷标。今后想要改卷标的话就用e2lable

mkfs.ext4 -L boot /dev/sdb1
...
mkswap -L swap /dev/sdb4

LFS的构建思路

LFS先利用宿主系统的构建工具构建自己工具链,存放在/mnt/lfs/tools,然后再利用这个工具链构建系统中的其他组件。一个问题就此而来:为什么不直接利用宿主系统的构建工具构建LFS的全部组件,而是先构建工具链?

这个问题在书的第5.2节给出了解答。简单的说就是减少宿主系统的头文件、动态静态库污染工具链,特别是工具链中的Binutils和GCC。Binutils和GCC会“记住”生成它们的库和头文件的路径。

创建中间工具链的目的应该在编译“最小Linux系统”的过程中认真体会。

创建宿主系统上的专用构建账号

groupadd lfs
useradd -s /bin/bash -g lfs -m -k /dev/null lfs

题外话,专用构建账户这种实践,在fedora包管理中也有。可以说这是一种基本的安全措施。

Login shell and a clean environment

Login shell通过su - [user]登入。与不加“-”号的su的主要区别是,这个加了“-“的版本会清除之前shell的环境变量。

但是这仍然不够,宿主系统的/etc/profile仍然会污染我们的lfs。因此需要借助exec(替换当前shell)和env -i(无视现有环境运行某个程序):

exec env -i HOME=$HOME TERM=$TERM PS1='\u:\w\$ ' /bin/bash

不过我不能理解的是为什么这句话写入.bash_profile才管用,直接在现有shell运行不完全起作用。

巧妙利用bash和cat创建新文件

书中多次出现了类似于

cat > ~/.bash_profile << "EOF"

的技巧。

bash的hash function

bash内置了一个hash表,它会保留上一次在PATH中找到的可执行程序的完整路径,从而避免每一次运行ls都去查一遍PATH确认ls是在/bin/ls这样的位置。但是,在构建LFS,我们希望总是能重新搜索PATH=/tools/bin:/bin:/usr/bin:有些程序在宿主机的/bin/usr/bin也存在,但是我们希望在中间工具链构建的过程中,尽可能使用位于/tools/bin的自行编译的版本。因此在构建中间工具链的环境中应该使用set +h关掉bash的这个功能。

umask

umask掩码规定了在创建新文件时,哪些权限是被删除的。例如,umask 022表示创建的新文件的实际权限为666-022=644(均为八进制),也就是rw-r–r–;创建的新目录的实际权限为777-022=755(均为八进制),也就是rwxr-xr-x。。

注意1:umask 022并不会去掉编译程序所生成文件的执行权限。

注意2:umask可能是bash内置命令,也可能是/usr/bin/umask。用command -v umask确定用的到底是哪个!

阴阳师爬塔与迷宫生成算法

最近的阴阳师缘结神活动新增了走迷宫的要素。几百层的迷宫不可能手动绘制吧?于是想到了是不是有迷宫生成算法

能查到的迷宫生成算法有三种:递归回溯、 随机prim算法、 递归分割。前两种属于“拆墙法”,而递归分割则是“造墙法”。先放一下结论:

这三种算法分别适合不同的迷宫情况,递归回溯适合于那种主线支线明显的游戏(如RPG),而递归分割则适合转角较少的游戏(如FPS和ACT),至于Prim,似乎适合最标准的迷宫游戏(随机Prim算法生成的迷宫分支较多,整体上更复杂也更自然)。

阴阳师这个迷宫给我感觉是用递归回溯生成的,因为有一条指向终点的非常明显的通路。

递归回溯本质上是深度优先搜算方法,其迷宫生成步骤为(引自 https://www.jianshu.com/p/f643b0a0b887 ):

  1. 初始化,建立一个所有单元格都被墙隔开的迷宫。
  2. 随机选择一个单元格作为起始点,以此单元格开始打通墙壁。
  3. 以当前单元格为基准,随机选择一个方向,若此方向的邻接单元格没有被访问过,则打通这两个单元格之间的墙壁,并将此邻接单元格作为当前单元格,重复步骤3。
  4. 若当前单元格的四个邻接单元格都已经被访问过,则退回到进入当前单元格的邻接单元格,且以此单元格为当前单元格,重复步骤3,4。
  5. 直到起始点单元格被退回,则算法结束。

为了记录已经访问过的单元格,迷宫生成程序会维护一个,每次进入一个新的单元格,就将这个单元格的坐标放入栈顶。这样回退操作所需要的坐标从栈顶弹出就可以了。

又看了一下维基百科,自己摸索着写出了演示代码(github)。写完后发现,其实用这个算法的话,生成起点和生成终点未必就是玩家走的起点和终点。因为这个算法生成的图中,通路上的任意两点必然是可达的,因此起点和终点可以任意设置。

使用Radicale建立你的私有日历和联系人同步服务器

换手机了,发现新手机完全不能兼容google play,看log是少一个权限无法申请。不知道是不是google单方面ban了华为,或者华为单方面ban了google。不管怎么样我想要同步我的google账号上的日历和联系人是不可能了。

就在我导出google日历和联系人的时候,我又开始在想那个问题了:数据不在自己的手上,有一天可能就找不回来了。能不能自己搭建一个私有云呢?说到私有云我立刻想到了Nextcloud,但是看到Nextcloud的各种php、数据库还有http服务器依赖之后我觉得太麻烦了——我的需求是日历同步,Nextcloud一套上来过于复杂了,而且不够轻量。

于是我找到了Radicale这个开始于学校作业的项目。这个项目基本上可以在任何能运行Python的地方运行。它支持的CalDAV协议。这种协议的客户端的话,PC上的邮件和日历app基本都支持这个协议,我用的是thunderbird。手机上的话,貌似华为不能直接支持这种协议,得装个DAVx5

继续阅读“使用Radicale建立你的私有日历和联系人同步服务器”