Minimal Linux 桌面操作系统构建指南

#引言

一个完整的 Linux 操作系统包括多少个组件?涉及多少个 deb 包?如何从零开始构建一个最小化的 Linux 系统?本文将带你了解最小化 Linux 系统的构建过程, 涵盖内核, 引导加载程序, 基本工具链和必要的用户空间组件。

#最小化 Linux 系统组件

  • Linux Kernel: Linux 内核是操作系统的核心, 负责管理硬件资源和提供系统调用接口。
  • libc & loaders: GNU C 库是 Linux 系统的标准 C 库, 提供基本的系统调用封装和标准库函数。
    • /lib/x86_64-linux-gnu/libc.so.6
    • /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    • /lib64/ld-linux-x86-64.so.2 -> ../lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
  • /sbin/init: 1 号进程, 第一个用户态进程, 负责启动其他进程和服务, 常见的类型有:
    • SysVinit
    • systemd
    • OpenRC
  • Shell: 提供命令行解释器, 常用的有 bash, ash, dash 等。
  • libc-bin: 提供与 C 库相关的二进制文件, 如 ldd, ldconfig 等。
  • coreutils: 提供基本的用户命令行工具, 如 ls, cp, mv, rm 等。
  • util-linux: 提供额外的系统管理工具, 如 su, login, mount, dmesg, fdisk 等。
  • procps-ng: 提供进程管理工具, 如 ps, top, free 等。
  • debianutils: 提供一些 Debian 特有的实用工具, 如 which, run-parts 等。
  • net-tools: 提供传统的网络管理工具, 如 ifconfig, netstat, arp, route 等。
  • iproute2: 提供网络管理工具, 如 ip, ss, tc 等。
  • wget: 用于从网络上下载文件的命令行工具。

#构建一个基本的 rootfs

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
#!/usr/bin/env bash
# build-rootfs.sh
# 构建最小 rootfs(从宿主系统拷贝二进制 + 自动收集依赖库)
#
# 用法:
# sudo ./build-rootfs.sh /path/to/rootfs [mode]
# mode: "glibc" (默认) | "busybox-static"
#
# 说明:
# - 默认 mode=glibc:拷贝指定的 coreutils/util-linux/procps 二进制并收集 ldd 依赖
# - mode=busybox-static:使用单个已编译静态 busybox(用户需在 HOST_BUSYBOX_PATH 提供)
#
set -euo pipefail

ROOTFS="${1:-./rootfs}"

# 可按需调整要拷贝的命令列表(优先用宿主系统的路径)
LIBC_BIN_CMDS=( \
/sbin/ldconfig \
/usr/bin/catchsegv \
/usr/bin/getconf \
/usr/bin/getent \
/usr/bin/iconv \
/usr/bin/ldd \
/usr/bin/locale \
/usr/bin/localedef \
/usr/bin/pldd \
/usr/bin/tzselect \
/usr/bin/zdump \
/usr/sbin/iconvconfig \
/usr/sbin/zic \
)
CORE_CMDS=( \
/bin/cat /bin/tac /usr/bin/nl /usr/bin/od \
/usr/bin/base32 /usr/bin/base64 /usr/bin/basenc \
/usr/bin/fmt /usr/bin/pr /usr/bin/fold \
/usr/bin/head /usr/bin/tail /usr/bin/split /usr/bin/csplit \
/usr/bin/wc /usr/bin/sum /usr/bin/cksum /usr/bin/md5sum \
/usr/bin/b2sum /usr/bin/sha1sum /usr/bin/sha224sum \
/usr/bin/sha256sum /usr/bin/sha384sum /usr/bin/sha512sum \
/usr/bin/sort /usr/bin/shuf /usr/bin/uniq /usr/bin/comm \
/usr/bin/ptx /usr/bin/tsort /usr/bin/cut /usr/bin/paste \
/usr/bin/join /usr/bin/tr /usr/bin/expand /usr/bin/unexpand \
/bin/ls /usr/bin/dir /usr/bin/vdir /usr/bin/dircolors \
/bin/cp /usr/bin/dd /usr/bin/install /bin/mv /bin/rm \
/usr/bin/shred /usr/bin/link /bin/ln /usr/bin/mkdir \
/usr/bin/mkfifo /usr/bin/mknod /usr/bin/readlink /usr/bin/rmdir \
/usr/bin/unlink /usr/bin/chown /usr/bin/chgrp /usr/bin/chmod \
/usr/bin/touch /usr/bin/df /usr/bin/du /usr/bin/stat \
/usr/bin/sync /usr/bin/truncate /bin/echo /usr/bin/printf \
/usr/bin/yes /usr/bin/false /usr/bin/true /usr/bin/test \
/usr/bin/expr /usr/bin/tee /usr/bin/basename /usr/bin/dirname \
/usr/bin/pathchk /usr/bin/mktemp /usr/bin/realpath /usr/bin/pwd \
/usr/bin/stty /usr/bin/printenv /usr/bin/tty /usr/bin/id \
/usr/bin/logname /usr/bin/whoami /usr/bin/groups /usr/bin/users \
/usr/bin/who /usr/bin/pinky /usr/bin/date /usr/bin/arch \
/usr/bin/nproc /usr/bin/uname /usr/bin/hostname /usr/bin/hostid \
/usr/bin/uptime /usr/bin/chcon /usr/bin/runcon /usr/bin/chroot \
/usr/bin/env /usr/bin/nice /usr/bin/nohup /usr/bin/stdbuf \
/usr/bin/timeout /usr/bin/kill /usr/bin/sleep /usr/bin/factor \
/usr/bin/numfmt /usr/bin/seq \
/usr/bin/\[ /usr/bin/test /usr/bin/expr \
)
UTIL_LINUX_CMDS=( \
/sbin/mount /sbin/umount /usr/bin/findmnt /usr/bin/mountpoint \
/sbin/losetup /usr/sbin/blkid /usr/bin/lsblk /sbin/fdisk \
/usr/sbin/sfdisk /usr/sbin/partx /usr/bin/swapon /usr/bin/swapoff \
/usr/sbin/mkswap /usr/bin/dmesg /sbin/hwclock /usr/bin/logger \
/usr/bin/wall /usr/bin/write /sbin/agetty /usr/bin/login \
/usr/bin/uuidgen /usr/sbin/uuidd /usr/bin/rename /usr/bin/col \
/usr/bin/colcrt /usr/bin/colrm /usr/bin/column \
/usr/bin/hexdump /usr/bin/hd /usr/bin/look /usr/bin/ul \
/usr/bin/chfn /usr/bin/chsh /usr/bin/chrt /usr/bin/taskset \
/usr/bin/lslogins /usr/bin/loginctl /usr/bin/fallocate \
/usr/bin/blockdev /usr/bin/mkfs /usr/bin/mkfs.bfs \
/usr/sbin/ctrlaltdel /usr/bin/su \
)
PROCPS_CMDS=( /usr/bin/ps /usr/bin/top /usr/bin/free /usr/bin/uptime )
NET_TOOLS_CMDS=( /sbin/ifconfig /sbin/ip /usr/sbin/ss /usr/sbin/netstat /usr/sbin/route )
SHELL_CANDIDATES=( /bin/bash /bin/sh /bin/ash /bin/dash )
NET_UTILS=( /sbin/ifconfig /sbin/ip /usr/sbin/ss /usr/sbin/netstat /usr/sbin/route )
EXTRA=( /bin/hostname /usr/bin/lsof /usr/bin/wget )

# Busybox (仅当 mode=busybox-static 且宿主提供时使用)
HOST_BUSYBOX_PATH="${HOST_BUSYBOX_PATH:-/usr/local/bin/busybox}" # 可覆盖

# helpers
info(){ printf '\e[1;32m[INFO]\e[0m %s\n' "$*"; }
warn(){ printf '\e[1;33m[WARN]\e[0m %s\n' "$*"; }
err(){ printf '\e[1;31m[ERROR]\e[0m %s\n' "$*"; exit 1; }

mkdir_p() { mkdir -p -- "$@"; }

# gather list of binaries to copy (resolve real paths)
collect_bins() {
local -n out=$1
out=()
for f in "${LIBC_BIN_CMDS[@]}" "${CORE_CMDS[@]}" "${UTIL_LINUX_CMDS[@]}" "${PROCPS_CMDS[@]}" "${NET_TOOLS_CMDS[@]}" "${EXTRA[@]}"; do
if [ -x "$f" ]; then
out+=("$f")
else
# try which
cmdname="$(basename "$f")"
path="$(command -v "$cmdname" 2>/dev/null || true)"
if [ -n "$path" ]; then
out+=("$path")
fi
fi
done

# ensure we have a shell and an init candidate
SHELL_BIN=""
for s in "${SHELL_CANDIDATES[@]}"; do
if [ -x "$s" ]; then
SHELL_BIN="$s"
break
fi
done
if [ -n "$SHELL_BIN" ]; then
out+=("$SHELL_BIN")
else
warn "No shell found on host; you'll need to provide one (bash/sh) or use busybox-static mode."
fi
}

# copy binary and its parent dir structure
copy_bin() {
local src="$1"
local dstroot="$2"
if [ ! -f "$src" ]; then
warn "binary not found: $src"
return
fi
local dst="$dstroot${src}"
mkdir_p "$(dirname "$dst")"
cp -a -- "$src" "$dst"
# preserve permissions
chmod --reference="$src" "$dst"
}

# collect shared libs via ldd and copy them
copy_libs_for_bin() {
local bin="$1"
local dstroot="$2"
# ldd may fail for statically linked; handle gracefully
if ldd_output="$(ldd "$bin" 2>/dev/null)" ; then
while IFS= read -r line; do
# lines like: linux-vdso.so.1 (0x00007fff...)
# or: libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f...)
# or: /lib64/ld-linux-x86-64.so.2 (0x...)
libpath=""
if [[ "$line" =~ "=>" ]]; then
libpath="$(echo "$line" | awk '{for(i=1;i<=NF;i++){ if ($i == "=>"){ print $(i+1); break }}}')"
else
# fallback: first token that starts with /
libpath="$(echo "$line" | awk '{for(i=1;i<=NF;i++){ if ($i ~ /^\//){ print $i; break }}}')"
fi
if [ -n "$libpath" ] && [ -f "$libpath" ]; then
# copy into same relative path under rootfs
mkdir_p "$dstroot$(dirname "$libpath")"
cp -a -- "$libpath" "$dstroot$libpath"

# if it's a symlink, also copy the target
if [ -L "$libpath" ]; then
target="$(readlink "$libpath")"
# handle relative symlinks
if [[ "$target" != /* ]]; then
target="$(dirname "$libpath")/$target"
fi
if [ -f "$target" ]; then
mkdir -p "$dstroot$(dirname "$target")"
cp -a -- "$target" "$dstroot$target"
fi
fi
fi
done <<< "$ldd_output"
else
# not a dynamic ELF or ldd failed
:
fi
}

create_basic_tree() {
local r="$1"
info "Creating base directories under $r"
mkdir_p "$r"/{bin,sbin,etc,proc,sys,dev,lib,lib64,usr,usr/bin,usr/sbin,tmp,var,root,home}
chmod 1777 "$r/tmp"
}

write_etc_files() {
local r="$1"
info "Writing /etc/passwd, /etc/group, /etc/inittab, /etc/fstab, /etc/profile"
cat > "$r/etc/passwd" <<'EOF'
root:x:0:0:root:/root:/bin/sh
EOF

cat > "$r/etc/group" <<'EOF'
root:x:0:
EOF

# minimal shadow (empty password; you can set hashed passwd if needed)
cat > "$r/etc/shadow" <<'EOF'
root:*:18500:0:99999:7:::
EOF
chmod 600 "$r/etc/shadow"

# fstab - auto mount proc/sys/devtmpfs if using our init scripts
cat > "$r/etc/fstab" <<'EOF'
proc /proc proc defaults 0 0
sysfs /sys sysfs defaults 0 0
devtmpfs /dev devtmpfs defaults 0 0
EOF

# simple profile
cat > "$r/etc/profile" <<'EOF'
export PATH=/bin:/sbin:/usr/bin:/usr/sbin
export HOME=/root
export TERM=${TERM:-vt100}
EOF

# simple /etc/inittab for busybox-style init (if using busybox init)
cat > "$r/etc/inittab" <<'EOF'
::sysinit:/etc/init.d/rcS
ttyS0::respawn:/bin/sh
::ctrlaltdel:/sbin/reboot
EOF
}

write_init_script() {
local r="$1"
info "Writing simple /etc/init.d/rcS and /sbin/init wrapper"
mkdir -p "$r/etc/init.d"
cat > "$r/etc/init.d/rcS" <<'EOF'
#!/bin/sh
set -eu

# mount pseudo fs if not mounted
mount -t proc proc /proc 2>/dev/null || true
mount -t sysfs sys /sys 2>/dev/null || true

# Prefer devtmpfs, otherwise bind host /dev if running from host
if ! mountpoint -q /dev; then
if mount -t devtmpfs devtmpfs /dev 2>/dev/null; then
:
else
echo "Warning: devtmpfs not available; try bind mounting /dev from host"
fi
fi

# devpts for pty allocation
if [ ! -d /dev/pts ] ; then
mkdir -p /dev/pts
fi
if ! mountpoint -q /dev/pts; then
mount -t devpts devpts /dev/pts -o gid=5,mode=620 2>/dev/null || true
fi

# ensure /tmp exists
mkdir -p /tmp
chmod 1777 /tmp

echo "Minimal rootfs boot complete."

# helper: try to spawn agetty on given device, return 0 on success
try_getty() {
dev=$1
if [ -c "/dev/${dev}" ] || [ -e "/dev/${dev}" ]; then
if [ -x /sbin/agetty ]; then
echo "Starting agetty on ${dev}..."
exec /sbin/agetty -L "${dev}" 115200 vt100
return 0
fi
fi
return 1
}

# Preferred tty devices to try (adjust order for your env)
for tty in tty1 ttyS0 console; do
# only try when device exists and is a character device (or present)
if [ -e "/dev/$tty" ]; then
try_getty "$tty" && exit 0
fi
done

# If no suitable tty or agetty absent, spawn a shell on current stdio
echo "No usable tty for getty found. Dropping to shell."
exec /bin/sh -l
EOF
chmod +x "$r/etc/init.d/rcS"

# Provide a PID 1 init that simply runs the rcS and then respawns a shell.
# If busybox is used as init, it will replace this.
cat > "$r/sbin/init" <<'EOF'
#!/bin/sh
# very small pid 1 wrapper
/bin/sh /etc/init.d/rcS
# if that returns, keep a shell on console
exec /bin/sh
EOF
chmod +x "$r/sbin/init"
}

copy_binaries_and_libs() {
local r="$1"
local -n bins_ref=$2
info "Copying binaries to $r and collecting shared libs"
for b in "${bins_ref[@]}"; do
info " -> $b"
copy_bin "$b" "$r"
copy_libs_for_bin "$b" "$r"
done
}

copy_loader() {
# copy dynamic linker (ld-linux*) if exists
local r="$1"
# try common locations
for ld in /lib64/ld-linux-x86-64.so.2 /lib/ld-linux.so.2 /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 /lib/ld-musl-x86_64.so.1; do
if [ -f "$ld" ]; then
mkdir -p "$r$(dirname "$ld")"
cp -a -- "$ld" "$r$ld"
fi
done
}

# ----------
# Main
# ----------
info "Build rootfs into: $ROOTFS (mode=glibc)"
if [ -e "$ROOTFS" ]; then
warn "$ROOTFS already exists — existing files may be overwritten"
fi

create_basic_tree "$ROOTFS"
write_etc_files "$ROOTFS"
write_init_script "$ROOTFS"

# glibc mode: collect binaries from lists
declare -a BINS
collect_bins BINS
# ensure we have at least a shell
if [ ${#BINS[@]} -eq 0 ]; then
warn "No binaries discovered to copy. Exiting."
exit 1
fi
copy_binaries_and_libs "$ROOTFS" BINS
copy_loader "$ROOTFS"

# ensure /bin/sh exists: prefer host shell or symlink to busybox if provided
if [ ! -e "$ROOTFS/bin/sh" ]; then
# try to find a shell binary inside rootfs
if [ -x "$ROOTFS/bin/bash" ]; then
ln -s /bin/bash "$ROOTFS/bin/sh"
elif [ -x "$ROOTFS/bin/dash" ]; then
ln -s /bin/dash "$ROOTFS/bin/sh"
elif [ -x "$ROOTFS/bin/ash" ]; then
ln -s /bin/ash "$ROOTFS/bin/sh"
elif [ -x "$ROOTFS/bin/$(basename "$HOST_BUSYBOX_PATH")" ]; then
ln -s /bin/$(basename "$HOST_BUSYBOX_PATH") "$ROOTFS/bin/sh"
else
warn "/bin/sh not found in rootfs. You may not be able to get a shell."
fi
fi

# finalize permissions
chmod 755 "$ROOTFS" || true
info "Rootfs build complete at $ROOTFS"

cat <<EOF

Next steps / hints:
- To test with qemu:
qemu-system-x86_64 -kernel /path/to/bzImage -initrd rootfs.cpio.gz -nographic -append "console=ttyS0 root=/dev/ram0 rdinit=/sbin/init"

- Or create an initramfs:
cd $ROOTFS
find . | cpio -H newc -o --owner root:root > ../rootfs.cpio
gzip -9 ../rootfs.cpio

- Or chroot into it (as root):
mount --bind /proc $ROOTFS/proc
mount --bind /sys $ROOTFS/sys
mount --bind /dev $ROOTFS/dev
chroot $ROOTFS /sbin/init

EOF

#启动 rootfs

1
2
3
4
5
#!/bin/sh
set -ex

unshare --mount-proc --uts --ipc --net --pid --fork --user --map-root-user chroot ./rootfs /sbin/init