ساختن کوچک‌ترین لینوکس شخصی من (از کرنل تا Lua)

نویسنده Leen 10 Dec 2025 · 12:00

دنیا پر از توزیع‌های لینوکسی آماده است. از اوبونتو و فدورا گرفته تا آرچ و نیکس و هزاران توزیع دیگر که فقط با چند کلیک بالا می‌آیند، دسکتاپ دارند و بدون دردسر کار می‌کنند. اما یک جایی در مسیر یادگیری سیستم‌عامل، انسان دلش می‌خواهد یک سؤال خیلی ساده بپرسد:

«حداقلِ کرنلی که لازم دارم تا یک لینوکس ۶۴ بیتی بالا بیاید چقدر است؟
اگر خودم همه‌چیز را از صفر بسازم یا تظیم کنم، این سیستم‌عامل چه شکلی می‌شود؟»

این مقاله روایت همین سؤال است؛ روایتی در سه مرحله:

  1. ساختن یک کرنل مینیمال و بوت‌کردنش در QEMU
  2. نوشتن init با Rust + اسمبلی، بدون libc و بدون std
  3. اضافه‌کردن Lua، ساخت initramfs واقعی و در نهایت ساختن یک ISO قابل بوت

این متن مکمل ویدیو است، نه فقط ترنسکریپت. اگر دوست داری عمیق‌تر وارد ماجرا شوی، اینجا جایی‌ست که تمام جزئیات را با حوصله باز می‌کنم.


چرا اصلاً چنین کاری کردم؟

ما معمولاً لینوکس را به شکل «یک توزیع» می‌بینیم:
اوبونتو، آرچ، دبیان، نیکس…
در حالی‌که در عمق، لینوکس بیشتر شبیه یک «پازل» است:

  • کرنل (kernel)
  • بوت‌لودر (bootloader)
  • فضای کاربر (user space)
  • ابزارها، شل، کتابخانه‌ها، زبان‌ها…

وقتی دست‌به‌کار می‌شوی و خودت یک سیستم کوچک می‌سازی، چند چیز تغییر می‌کند:

  • دیگر سیستم‌عامل را «جادو» نمی‌بینی؛
    می‌فهمی از کجا شروع می‌شود و چطور لایه‌لایه ساخته می‌شود.
  • بهتر می‌فهمی ابزارهایی مثل Docker، QEMU، initramfs، systemd و… روی چه زمینی ایستاده‌اند.
  • مهم‌تر از همه: حس مالکیتت نسبت به سیستم‌ات عوض می‌شود.

این پروژه قرار نیست یک دیسترو «کاربردی» برای استفاده روزمره باشد؛
بیشتر یک میکروسکوپ آموزشی است برای دیدن ساده‌ترین فرم یک سیستم‌عامل.


بخش اول – ساختن کوچک‌ترین کرنل ممکن و بوت در QEMU

در بخش اول، هدف این بود:

  • سورس کرنل را بگیرم
  • یک تنظیمات ultra-minimal بسازم (tinyconfig)
  • چند گزینه‌ی مهم را دستی تنظیم کنم
  • و در نهایت کرنل را بوت کنم تا به آن پیام معروف برسم:
    no working init found

گرفتن سورس کرنل

من زیر ~/_dev/smallest_linux یک پوشه برای کرنل در نظر گرفتم:

cd ~/_dev/smallest_linux
git clone https://github.com/torvalds/linux.git
cd linux

برای اطمینان:

pwd
ls

ساختار پوشه‌ها (arch, fs, kernel, drivers و…) همان کرنلی است که همه‌جا می‌بینیم و ازش می‌ترسیم =)

استفاده از tinyconfig – کوچک‌ترین کانفیگ ممکن

کرنل خودش یک هدف آماده برای ساختن کانفیگ‌های «خیلی کوچک» دارد:

make tinyconfig

این دستور یک .config فوق مینیمال می‌سازد. اما:

  • من ۶۴ بیتی می‌خوام
  • initramfs لازم دارم
  • لاگ و کنسول متنی لازم داریم

پس باید با menuconfig کمی تنظیمات را دست‌کاری کنم:

make menuconfig

چند تنظیم حیاتی که انجام دادم:

۱. فعال کردن کرنل ۶۴‌بیتی

در منوهای مربوط (بسته به نسخه‌ی کرنل):

  • Processor type and features یا بخش مشابه
  • گزینه‌ی 64-bit kernel را فعال کردم.

۲. فعال‌کردن initramfs

از General setup:

  • Initial RAM filesystem and RAM disk (initramfs/initrd) support

این برای ما حیاتی است، چون بعداً کل init و Lua را از طریق initramfs به کرنل می‌دهیم.

۳. فعال‌کردن printk

باز در General setup، زیر گزینه‌های «expert»:

  • Enable support for printk

اگر این گزینه را خاموش بذاریم، کرنل هیچ‌چیزی روی صفحه چاپ نمی‌کند و عملاً به کار این پروژه نمی‌آید.

۴. فعال‌کردن TTY / کنسول

در Device DriversCharacter devices، گزینه‌های مربوط به Virtual terminal / TTY / Console on virtual terminal را فعال کردم. بدون این‌ها، خروجی متنی نداریم.

۵. فعال‌بودن ELF binary support

در Executable file formats / Emulations، گزینه‌ی Kernel support for ELF binaries رو روشن کردم. چون init من یک ELF واقعی است؛ اگر این گزینه روشن نباشد، کرنل اصلاً توانایی اجرای اجرا آن را ندارد.

بعد از این تغییرها، از menuconfig خارج شو و ذخیره کن تا .config نهایی شود.

ساخت کرنل

حالا:

make -j 5

یا:

make -j $(nproc)

بعد از بیلد:

ls -lh arch/x86/boot/bzImage

همین bzImage همان هسته‌ی اصلی دنیای کوچک ماست.

بوت اولیه با QEMU (بدون initramfs)

برای این که ببینیم کرنل بالا می‌آید:

qemu-system-x86_64 \
  -kernel arch/x86/boot/bzImage \
  -m 64M

روی صفحه کلی پیام می‌بینی؛ مهم‌ترین بخش در انتهاست:

Run /sbin/init as init process
...
No working init found. Try passing init= option to kernel.
Kernel panic - not syncing: Attempted to kill init!

در این لحظه:

  • کرنل بالا آمده
  • مموری، CPU، و بقیه‌ی موارد را تنظیم کرده
  • فقط init ندارد، و کاملاً منطقی panic می‌کند

این همان جایی است که پارت اول تمام می‌شود و سؤال بعدی شکل می‌گیرد:

اگر init را خودم بنویسم، چه؟


بخش دوم – نوشتن یک init مینیمال با Rust + اسمبلی

در پارت دوم، هدف این بود:

  • یک باینری فوق‌العاده کوچک به نام init بسازم
  • بدون std، بدون libc، بدون main
  • مستقیم با system callها حرف بزنم
  • و این init را به‌عنوان PID 1 روی کرنل خودم بالا بیارم

در اینجا، Rust بیشتر شبیه زبان اسمبلی سطح بالا استفاده می‌شود تا یک زبان راحت سطح بالا.

طراحی کلی init

طراحی منطقی این init:

  • نقطه‌ی ورود: _start

  • چاپ چند پیام خوش‌آمدگویی

  • وارد یک حلقه‌ی شبیه شل شود:

    • "$ " چاپ کند

    • از stdin یک خط بخواند

    • newline را حذف کند

    • اگر خالی بود، continue

    • اگر نه،:

      • fork کند
      • در child → execve(path, argv, envp)
      • در parent → wait4 برای فرزند

به خاطر این که هیچ PATH و محیط استانداردی ندارم، حتماً باید مسیر کامل را می‌نوشتم، مثلاً /bin/lua.

کد Rust بدون std و بدون main

ایده این است:

  • #![no_std] → یعنی هیچ کتابخانه‌ی استاندارد
  • #![no_main] → یعنی نقطه‌ی ورود خودمان را تعریف می‌کنیم
  • سیستم‌کال‌ها را با extern "C" اعلان می‌کنیم و در اسمبلی پیاده‌سازی

کد اصلی (خلاصه‌شده) چیزی شبیه این است:

#![no_std]
#![no_main]

use core::panic::PanicInfo;

extern "C" {
    fn sys_write(fd: isize, buf: *const u8, len: usize) -> isize;
    fn sys_read(fd: isize, buf: *mut u8, len: usize) -> isize;
    fn sys_fork() -> isize;
    fn sys_execve(path: *const u8,
                  argv: *const *const u8,
                  envp: *const *const u8) -> isize;
    fn sys_wait4(pid: i32,
                 status: *mut i32,
                 options: i32,
                 rusage: *mut u8) -> isize;
    fn sys_exit(code: i32) -> !;
}

#[no_mangle]
pub extern "C" fn _start() -> ! {
    write_str(b"\n\n[init] Rust minimal init started.\n");
    write_str(b"[init] Type a full path to exec (e.g. /bin/lua).\n\n");
    write_str(b"_______________________________\n");

    shell_loop();
}

fn shell_loop() -> ! {
    let mut buf = [0u8; 256];

    loop {
        write_str(b"-$ ");

        let n = unsafe { sys_read(0, buf.as_mut_ptr(), buf.len()) };
        if n <= 0 {
            unsafe { sys_exit(0) }
        }

        let mut n = n as usize;
        n = strip_newline(&mut buf[..n]);
        if n == 0 {
            continue;
        }

        let path_ptr = buf.as_ptr();
        let argv: [*const u8; 2] = [path_ptr, core::ptr::null()];

        let pid = unsafe { sys_fork() };

        if pid == 0 {
            unsafe {
                sys_execve(path_ptr, argv.as_ptr(), core::ptr::null());
                write_str(b"[child] execve failed, exiting.\n");
                sys_exit(1);
            }
        } else if pid > 0 {
            let mut status: i32 = 0;
            unsafe {
                sys_wait4(-1, &mut status as *mut i32, 0, core::ptr::null_mut());
            }
        } else {
            write_str(b"[parent] fork failed.\n");
        }
    }
}

fn strip_newline(buf: &mut [u8]) -> usize {
    let mut len = buf.len();
    while len > 0 && (buf[len - 1] == b'\n' || buf[len - 1] == b'\r') {
        len -= 1;
    }
    if len < buf.len() {
        buf[len] = 0;
    }
    len
}

fn write_str(s: &[u8]) {
    unsafe {
        sys_write(1, s.as_ptr(), s.len());
    }
}

#[panic_handler]
fn panic(_: &PanicInfo) -> ! {
    unsafe { sys_exit(1) }
}

اینجا هیچ خبری از println!، heap، Vec، String و… نیست. همه‌چیز روی یک بافر ثابت و چند system call ساده سوار شده.

لایه‌ی اسمبلی: پیاده‌سازی system callها

برای این که sys_write و بقیه واقعاً کار کنند، باید آن‌ها را در یک فایل ASM پیاده کنیم:

.section .text
.intel_syntax noprefix

.globl sys_write
.globl sys_read
.globl sys_fork
.globl sys_execve
.globl sys_wait4
.globl sys_exit

# isize sys_write(isize fd, const u8 *buf, usize len)
sys_write:
    mov rax, 1          # __NR_write
    syscall
    ret

# isize sys_read(isize fd, u8 *buf, usize len)
sys_read:
    mov rax, 0          # __NR_read
    syscall
    ret

# isize sys_fork(void)
sys_fork:
    mov rax, 57         # __NR_fork
    syscall
    ret

# isize sys_execve(const u8 *path, const u8 **argv, const u8 **envp)
sys_execve:
    mov rax, 59         # __NR_execve
    syscall
    ret

# isize sys_wait4(i32 pid, i32 *status, i32 options, u8 *rusage)
sys_wait4:
    mov rax, 61         # __NR_wait4
    syscall
    ret

# void sys_exit(i32 code) -> !
sys_exit:
    mov rax, 60         # __NR_exit
    syscall
.Lhang:
    jmp .Lhang
  • شماره‌ها (0, 1, 57, 59, 60, 61) همان شماره‌های سیستم‌کال روی x86_64 هستند.
  • آرگومان‌ها طبق ABI لینوکس در rdi, rsi, rdx, rcx, r8, r9 می‌آیند.
  • من فقط rax را ست می‌کنم و syscall می‌زنم.

کامپایل و لینک به یک باینری استاتیک non-PIE

ابتدا object اسمبلی را بساز:

as --64 syscalls.s -o syscalls.o

بعد با rustc مستقیماً لینک کن:

rustc \
  -C opt-level=z \
  -C lto \
  -C panic=abort \
  -C relocation-model=static \
  -C link-arg=-nostartfiles \
  -C link-arg=-static \
  -C link-arg=-no-pie \
  -C link-arg=syscalls.o \
  shell.rs -o init

و در نهایت:

strip init
file init
ldd init || echo "ldd failed (probably static / no interpreter)"

الان یک init دارم که:

  • استاتیک است
  • non-PIE است
  • هیچ وابستگی داینامیکی ندارد

ساخت initramfs مینیمال فقط با init

یک rootfs ساده:

mkdir -p rootfs
cp init rootfs/init

cd rootfs
find . | cpio -H newc -o > ../init.cpio

و بعد:

cp init.cpio ~/_dev/linux/

بوت کرنل با init خودم

دوباره به کرنل برگرد:

cd ~/_dev/linux

qemu-system-x86_64 \
  -kernel arch/x86/boot/bzImage \
  -initrd ./init.cpio \
  -append "console=ttyS0 loglevel=3 quiet" \
  -m 64M

اگر همه‌چیز درست باشد، این‌بار خبری از no working init found نیست. باید چیزی شبیه این نمایش داده بشه:

[init] Rust minimal init started.
[init] Type a full path to exec (e.g. /bin/lua).

-$

در این لحظه، کرنل tiny خودم در حال اجراست و init نوشته‌شده با Rust خودم، PID 1 سیستم است.


بخش سوم – اضافه کردن Lua و ساخت یک ISO کامل

تا اینجا:

  • کرنل دارم
  • init دارم
  • initramfs دارم

ولی هیچ user space واقعی ندارم. در پارت سوم هدف این بود که:

  • یک برنامه واقعی (Lua) را استاتیک بسازم
  • آن را کنار init در rootfs قرار بدم
  • یک initramfs درست و حسابی بسازم
  • و در نهایت، با ابزار داخلی کرنل، یک ISO قابل بوت بسازیم.

کامپایل استاتیک Lua

در ~/_dev:

cd ~/_dev/smallest_linux/
wget https://www.lua.org/ftp/lua-5.4.7.tar.gz
tar xvf lua-5.4.7.tar.gz
cd lua-5.4.7

make clean
make linux MYLDFLAGS="-static"

و بعد:

file src/lua
ldd src/lua
strip src/lua
ls -lh src/lua

این باینری lua، یک ELF استاتیک است؛ دقیقاً همان چیزی که می‌خواهیم.

ساخت rootfs نهایی (init + Lua)

در پروژه‌ی Rust:

cd ~/_dev/smallest_linux/shell_min
mkdir -p rootfs/bin

cp init rootfs/init
cp ~/ _dev/smallest_linux/lua-5.4.7/src/lua rootfs/bin/lua

cd rootfs
find . -print

ساختار:

.
./init
./bin
./bin/lua

حالا یک فضای کاربر کوچک دارم: init و Lua.

ساخت initramfs واقعی

در rootfs:

find . | cpio -H newc -o > ../init.cpio

و دوباره کپی:

cd ..
cp init.cpio ~/_dev/smallest_linux/linux/

ساخت ISO با make isoimage

حالا در سورس لینوکس:

cd ~/_dev/smallest_linux/linux

make isoimage \
  FDARGS="loglevel=3 quiet initrd=init.cpio" \
  FDINITRD=/home/snape/_dev/smallest_linux/linux/init.cpio

نتیجه:

ls -lh arch/x86/boot/image.iso

این همان ISO است که:

  • کرنل tiny تو
  • initramfs خودت
  • و init + Lua را در خودش دارد.

بوت ISO در QEMU

qemu-system-x86_64 \
  -cdrom arch/x86/boot/image.iso \
  -m 64M

اگر همه‌چیز درست باشد، همان پیام‌های init ظاهر می‌شود و prompt می‌رسد.

حالا وقت اجرای Lua است:

-$ /bin/lua
Lua 5.4.7  Copyright ...
>

و مثلاً:

> for i=1,10 do print("hello", i) end

این لحظه، نقطه‌ی اوج پروژه بود:

  • کرنلی که خودم ساختم
  • initی که خودم با Rust و ASM نوشتم
  • initramfsی که خودم پک کردم
  • و زبان Lua که روی این «جهان کوچک» اجرا می‌شود

همه‌ی این‌ها در چند 3.4 مگابایت جا شده‌اند.


جمع‌بندی – وقتی سیستم‌عامل از «ابزار آماده» به «اثر شخصی» تبدیل می‌شود

این پروژه قرار نیست جای هیچ توزیعی را بگیرد، قرار نیست یک سیستم‌عامل «مصرفی» برای استفاده‌ی روزمره بسازد.

چیزی که برای من مهم بود، این بود که:

  • ببینم کوچک‌ترین لینوکسی که واقعاً می‌فهمم چطور کار می‌کند، چه شکلی است
  • از لایه‌ی کرنل، تا init، تا اجرای یک برنامه‌ی واقعی مثل Lua
  • و این مسیر را تا انتها خودم برم، نه فقط با خواندن مستندات

بعد از این سفر:

  • کرنل دیگر فقط یک «هستهٔ ترسناک» نیست؛ یک فایل ۱–۲ مگی است که خودم ساختم
  • init دیگر فقط یک پروسه‌ی ناشناس با PID 1 نیست؛ یک برنامه‌ی Rust است که خودم نوشتم
  • و «سیستم‌عامل» دیگر فقط لوگوی یک شرکت یا اسم یک دیسترو نیست؛ چیزی است که می‌تونم خودم هم بسازمش، هرچند در ابعاد کوچک.

دنیا پر از دیستروهای آماده است؛ اما یک بار ساختن کوچک‌ترین دنیای لینوکسی خودت، چیزی است که دیدت را نسبت به همه‌ی آنها عوض می‌کند.

دیدگاه‌ها

اولین نفری باشید که دیدگاه می‌گذارد.

دیدگاه بگذارید