# RISCV with TinyEMU contributed by < `johnnylord` > < `yiwei` > ## 預期目標 - 熟悉 GNU Toolchain 相關開發工具 - 接觸 RISC-V 處理器架構 - 客製化 Buildroot - 學習系統模擬器的內部運作機制 ## 檢查清單 ### riscv-emu 原始程式碼中多次出現 [virtio](https://www.linux-kvm.org/page/Virtio),這樣的機制對於 host 和 guest 兩端有何作用?在閱讀 [Virtio: An I/O virtualization framework for Linux](https://www.ibm.com/developerworks/library/l-virtio/index.html) 一文後,對照原始程式碼,你發現什麼? 在探討 virtio 之前,需要先了解 virtualization 有兩種類型的虛擬化方案,一則是 full virtualization,另一則是 paravirtualization。 - full virtualization : guest OS 運行在 hypervisor 之上,每當 guest OS 欲執行存取硬體資源的指令 (例如 I/O), hypervisor 會捕獲這個 request ,並模擬出這些指令的行為,讓 guest OS 以為自己是直接存取硬體資源(由 hypervisor 負責 Device emulation )。在這種模式下,每次 I/O 操作的路徑比較長,效能不佳。另外, guest OS 不知自己其實是被虛擬化出來的,所以在模擬器上運行的 guest OS 不需要多做額外的修改。 - paravirtualization : guest OS 知道自己是運行在 hypervisor 之上,所以會與 hypervisor 合作,使得 Device emulation 更有效率。guest OS 與 hypervisor 合作的方式,便是修改 guest OS,使其包含 Para-drivers ,實做不同裝置的驅動程式,視為前端 ; 而 hypervisor 則是負責實做「對應裝置的功能模擬」的驅動程式 ,視為後端。然而,不同 hypervisor(Kernel-based Virtual Machine (KVM)、 lguest、User-mode Linux) 的 Device emulation 機制各不相同,為了有統一的標準化界面,virtio 提供一個各 hypervisor 都通用的前端接口 ,增加了跨平台間的程式碼可重用性,可以視為硬體的抽象概念。 > Figure1:full virtualization 與 paravirtualization 的運作環境。 ![](https://i.imgur.com/9uEnl2c.png) > Figure2:透過 virtio,guest OS 有統一的裝置驅動程式(Front-end drivers),而各 hypervisor 的 Back-end drivers 不須統一,只須實現 Front-end drivers 所需的對應行為即可。 ![](https://i.imgur.com/RJVLyVS.png) virtio 定義了 2 個層次來支持 guest-to-hypervisor 的溝通。第一層為 virtio ,它是一個連接 front-end drivers 和 back-end drivers 的 virtual queue interface。可見下圖 figure3. ![](https://i.imgur.com/RXmhhKu.png) ### 透過 `temu root-riscv64.cfg`, 我們在 RISCV/Linux 模擬環境中,可執行 gcc 並輸出對應的執行檔,而之後我們則執行 `riscv64-buildroot-linux-gnu-gcc`,這兩者有何不同? (提示: cross-compiler, 複習 [你所不知道的 C 語言: 編譯器和最佳化原理篇](https://hackmd.io/s/Hy72937Me) 由於我們實驗模擬的環境,CPU 架構為 riscv64,而我的電腦本身是 Intel x86_64 的電腦架構。在host 端用原先 `gcc` 編譯的程式碼,所產生的執行檔是給 x86_64 電腦架構執行,而 `riscv64-buildroot-linux-gnu-gcc` 則是一個 `cross-compiler`,我們先在 host 端使用這個 `cross-compiler` 編譯一個執行檔,這個執行檔可以執行在 `riscv64` 的平台上。 以下做的小小實驗,利用 cross-compiler 和 gcc 編譯一個小程式,看其編譯輸出 ```clike // test.c #include <stdio.h> int main() { printf("Hello World\n"); return 0; } ``` ```shell ## compile code using gcc gcc -o x86 test.c -static riscv64-buildroot-linux-gnu-gcc -o riscv test.c -static ``` 利用 `file` 指令觀察輸出檔 ```shell $ file riscv riscv: ELF 64-bit LSB executable, UCB RISC-V, version 1 (SYSV), statically linked, for GNU/Linux 4.15.0, with debug_info, not stripped $ file x86 x86: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=eecd167a919b2800d64854b2b105e382b968b73b, not stripped ``` 如果你用 `objdump -d` 將執行檔做反組譯,你會發現在原系統可以,但是 `riscv` 則不行。 ```shell $ objdump -d riscv riscv: file format elf64-little objdump: can't disassemble for architecture UNKNOWN! ``` 這個結果很直觀,由於反組譯是根據那個系統架構的指令集,所以在 x86 的環境下反組譯 riscv 的執行檔當然無法成功。 ### 在 Guest 端透過 `$ dmesg | grep 9pnet` 命令,我們可發現 `9P2000` 字樣,這和上述 VirtFS 有何關聯?請解釋運作原理並設計實驗 ### 在 [TinyEMU System Emulator by Fabrice Bellard](https://bellard.org/tinyemu/readme.txt) 提到 “Network block device”,你能否依據說明,嘗試讓 guest 端透過 host 存取到網際網路呢? `Tinyemu` 透過 TUN/TAP 界面,實做了讓 guest 透過 host 存去到網際網路的功能。 不過在講解 TUN/TAP 前,先了解一般情況電腦存取網路的方式 ![](https://i.imgur.com/4lZww9G.png) - Switch(Physical Ethernet) switch 裝置常整合到 modem 中,switch 負責網路封包傳輸中 Layer2 的部份,處理 Ethernet frame,決定將封包送到那一個 physical port; modem 負責網路封包傳輸中 Layer1 的部份,將封包送網外部網路(實現存取網際網路的意思) - NIC(Network interface card) 網路介面卡(NIC)透過 RJ4 Cable 連接到 switch。 而虛擬化的世界中,VM 存取網路的架構如下 ![](https://i.imgur.com/eTKlXGw.png) 淺灰色部份可以為 host 端主機,內部透過虛擬化(軟體實現) switch 和 NIC,可以讓 host 端裡面的 VM 透過 host 存取到網路。其中 Linux bridge 實現了 switch。 這裡終於可以進到 TUN/TAP 了。TUN/TAP 是一個 linux kernel 支援的 driver,讓 user space 的程式能夠存取 Layer2/3 的封包資訊。那為什麼需要它呢? 因為在實體的 NIC 接受到封包時,kernel 會處理 Ethernet frame 並將其從封包中捨去。如此一來,傳遞到虛擬的 NIC 時,得到的封包就不完整。而 TUN/TAP 的功用就是告訴 Linux bridge(存在於 kernel space 中)將 Ethernet frame 的資訊保留並直接傳遞給虛擬的 NIC。 所以根據 `Tinyemu` 專案下的 `netinit.sh` 虛擬網路建構腳本 ```bash # host network interface connected to Internet (change it) internet_ifname="enx00e04c68197a" # setup bridge interface ip link add br0 type bridge # create and add tap0 interface to bridge ip tuntap add dev tap0 mode tap user $USER ip link set tap0 master br0 ip link set dev br0 up ip link set dev tap0 up ifconfig br0 192.168.3.1 # setup NAT to access to Internet echo 1 > /proc/sys/net/ipv4/ip_forward # delete forwarding reject rule if present #iptables -D FORWARD 1 iptables -t nat -A POSTROUTING -o $internet_ifname -j MASQUERADE ``` 會建構好 linux bridge 並增加 TAP 類別的虛擬裝置。而 `Tinyemu` 也就可以和 `tap0` 溝通,傳輸/接收網路封包。在 `temu.c` 中可以看到模擬器和 `tap0` 裝置連結。 ```clike #if !defined(_WIN32) && !defined(__APPLE__) if (!strcmp(p->tab_eth[i].driver, "tap")) { p->tab_eth[i].net = tun_open(p->tab_eth[i].ifname); if (!p->tab_eth[i].net) exit(1); } else #endif { fprintf(stderr, "Unsupported network driver '%s'\n", p->tab_eth[i].driver); exit(1); } ``` 最後根據 [Tinyemu's README](https://bellard.org/tinyemu/readme.txt),執行完 `netinit.sh` 後,啟動模擬器,並在模擬器中執行 ```bash ifconfig eth0 192.168.3.2 route add -net 0.0.0.0 gw 192.168.3.1 eth0 ``` 就可以存取網路了 ![](https://i.imgur.com/mBFrxt3.png) ### 最初實驗輸入 `$ temu https://bellard.org/jslinux/buildroot-riscv64.cfg`,然後就能載入 RISC-V/Linux 系統,背後的原理是什麼呢?請以 VirtIO 9P 檔案系統和 riscv-emu 對應的原始程式碼來解說 要啟動一個系統最起碼需要以下元件(不考慮類 MCU 等系統) - Bootloader - Kernel - Root file system 透過觀察 `Tinyemu` 提供的相關設定檔(`*.cfg`) ``` /* VM configuration file */ { version: 1, machine: "riscv64", memory_size: 128, bios: "bbl64.bin", kernel: "kernel-riscv64.bin", cmdline: "console=hvc0 root=/dev/vda rw", drive0: { file: "root-riscv64.bin" }, /* Also access to the /tmp directory. Use mount -t 9p /dev/root /mnt to access it. */ fs0: { tag: "/dev/root", file: "/tmp" }, eth0: { driver: "tap", ifname: "tap0" }, } ``` 不難發現 `bios`, `kernel`, `drive0` 就分別指定了這些必須的要件。不過現在設定檔是來自網路 `https://.../buildroot-riscv64.cfg`。 模擬器透過網路載入設定檔的流程大致如下 - ==`temu.c:virt_machine_load_config_file`== 建構傳遞在後續函式間的 logging 資料結構(`VMConfigLoadState`)開始執行載入動作 - ==`machine.c:config_load_file`== 循序的呼叫載入的相關函式並註冊 Callback 函式 - ==`fs_wget.c:fs_wget`== `curl` 下載任務的建立 - ==`fs_wget.c:fs_wget2`== 將 `curl` 下載任務加入任務工作池中 - ==`temu.c:fs_net_event_loop`== - ==`fs_wget.c:fs_net_set_fdset`== 從 `curl` 任務池中選出任務,並坐下載。 在上面的流程中,程式邏輯中出現大量的 Callback 技巧。以下是在 `Tinyemu` 中 callback 技巧的整理 1. 當某項任務的執行(如載入模擬器設定檔)跨越多個函式,或跨越一段時間,通常會建構一個 wrapper class,將傳遞函式之間的資料打包成一個單元。 > 如 `VMConfigLoadState`,當 virtual machine 在載入設定檔時,過程中會執行一連串的 Callback 函式,而在個個函式中,virtual machine 載入的狀態都紀錄在 `VMConfigLoadState` 中,並傳遞在函式之間。 2. Callback 界面的 convention。`callback(void *opaque)`,`func(..., callback, opaque)` 每當一個 `curl` 下載結束時,都會執行 `config_file_load_cb` Callback 函式。 ```cpp /* * opaque -> VMConfigLoadState 代表 VM 當前載入狀態 * err -> curl 任務是否順利完成 * data -> 下載的資料內容 * size -> 下載的資料大小 */ static void config_load_file_cb(void *opaque, int err, void *data, size_t size) { VMConfigLoadState *s = opaque; if (err < 0) { vm_error("Error %d while loading file\n", -err); exit(1); } s->file_load_cb(s->file_load_opaque, data, size); } ``` `config_file_load_cb` 又會繼續執行其他 Callback `config_file_loaded` 做下載的檔案內容解析。 ```cpp static void config_file_loaded(void *opaque, uint8_t *buf, int buf_len) { VMConfigLoadState *s = opaque; VirtMachineParams *p = s->vm_params; if (virt_machine_parse_config(p, (char *)buf, buf_len) < 0) exit(1); /* load the additional files */ /* such as root file system */ s->file_index = 0; config_additional_file_load(s); } ``` 解析的內容為以下 ```json /* VM configuration file */ { version: 1, machine: "riscv64", memory_size: 256, bios: "bbl64.bin", kernel: "kernel-riscv64.bin", cmdline: "loglevel=3 swiotlb=1 console=hvc0 root=root rootfstype=9p rootflags=trans=virtio ro TZ=${TZ}", fs0: { file: "https://vfsync.org/u/os/buildroot-riscv64" }, eth0: { driver: "user" }, } ``` 可以看到 ==bios==, ==kernel== 都指定了相關的檔案(指定在 host 端),而 ==fs0:file== 則是 https url,指定遠端的 file system。所以之後還要再下載這個檔案。 在解析上面的設定檔中,就會先做初步的 VM 初始化,透過 `machine` 屬性,得知模擬 `riscv64` 架構的虛擬機器。由於 `Tinyemu` 可以模擬都個機器,它定義了統一的 VM 界面。 ```cpp struct VirtMachineClass { const char *machine_names; void (*virt_machine_set_defaults)(VirtMachineParams *p); VirtMachine *(*virt_machine_init)(const VirtMachineParams *p); void (*virt_machine_end)(VirtMachine *s); int (*virt_machine_get_sleep_duration)(VirtMachine *s, int delay); void (*virt_machine_interp)(VirtMachine *s, int max_exec_cycle); bool (*vm_mouse_is_absolute)(VirtMachine *s); void (*vm_send_mouse_event)(VirtMachine *s1, int dx, int dy, int dz, unsigned int buttons); void (*vm_send_key_event)(VirtMachine *s1, bool is_down, uint16_t key_code); }; ``` 而不同的機器要實做背後的邏輯,如 `riscv_machine_class` ```cpp const VirtMachineClass riscv_machine_class = { "riscv32,riscv64,riscv128", riscv_machine_set_defaults, riscv_machine_init, riscv_machine_end, riscv_machine_get_sleep_duration, riscv_machine_interp, riscv_vm_mouse_is_absolute, riscv_vm_send_mouse_event, riscv_vm_send_key_event, }; ``` 以上大概是設定檔下載的流程,不過 root file system 還沒建立完成,以下開始探討 `Tinyemu` 如何建立 P9 file sytem。 --- 當執行到 ==temu.c:fs_net_init==,會繼續建構 P9 file system。由於細節太多,描述全部容易失焦,以下就大略描述建構的過程。 大部分檔案系統不管是 ext2, ext3, P9,它們都要管理整個檔案系統中的檔案,而檔案的資訊就是 **inode**。所以第一步驟就是建立 root file system 中背後所有 inode 的資訊。 過程中會再下載額外關於 root file system 的資訊。 1. 整體 root file system 的資訊 https://vfsync.org/u/os/buildroot-riscv64/head?nocache=1 ``` Version: 1 Revision: 3 NextFileID: 1fa3 FSFileCount: 8092 FSSize: 209223680 FSMaxSize: 1073741824 Key: RootID: 1fa2 ``` 2. root file system 架構 https://vfsync.org/u/os/buildroot-riscv64/files/0000000000001fa2 ``` Version: 1 040755 0 0 1536506432.658617668 etc 100600 0 0 243 1536499328 shadow 2 100644 0 0 116 1536499330 os-release 3 040755 0 0 1536499329 init.d 100755 0 0 423 1534684844 rcK 4 100755 0 0 408 1534684844 rcS 5 100755 0 0 359 1534684844 S40network 6 100755 0 0 649 1534680003 S01logging 7 100755 0 0 1354 1534680169 S50dropbear 8 100755 0 0 1630 1536492638 S10udev 9 100755 0 0 1321 1534684844 S20urandom a . ... ``` 由於檔案過大我就不列出全部,可以看到它提供我們關於遠端系統中所有檔案的資訊,權限,型態,時間,等等。這邊大概解釋一下如何解讀內容 ![](https://i.imgur.com/rAUJXH7.png) 有了這兩個檔案,就可以從原本空空的檔案系統 ![](https://i.imgur.com/7QXghyn.png) 慢慢建構出完整的檔案系統 ![](https://i.imgur.com/G0nnkoX.png) 而 P9 file system 操作的實做也和 VM 差不多,由於 `Tinyemu` 也預期支援其他 file system type,所以它也訂了ㄧ個統一的界面 ```cpp // fs.h struct FSDevice { void (*fs_end)(FSDevice *s); void (*fs_delete)(FSDevice *s, FSFile *f); void (*fs_statfs)(FSDevice *fs, FSStatFS *st); int (*fs_attach)(FSDevice *fs, FSFile **pf, FSQID *qid, uint32_t uid, const char *uname, const char *aname); // ... int (*fs_readlink)(FSDevice *fs, char *buf, int buf_size, FSFile *f); int (*fs_renameat)(FSDevice *fs, FSFile *f, const char *name, FSFile *new_f, const char *new_name); int (*fs_unlinkat)(FSDevice *fs, FSFile *f, const char *name); int (*fs_lock)(FSDevice *fs, FSFile *f, const FSLock *lock); int (*fs_getlock)(FSDevice *fs, FSFile *f, FSLock *lock); }; ``` 而 P9 file system 就是要實現這些操作,這些操作定義都在 `fs_net.c` 檔案中。 所以經過了一連串的載入和初始化 ==bootloader==, ==kernel==, ==root file system== 都完備了,也就可以進入系統啟動的階段了。 ### riscv-emu 內建浮點運算模擬器,使用到 [SoftFP Library](https://bellard.org/softfp/),請以 `sqrt` 為例,解說 `sqrt_sf32`, `sqrt_sf64`, `sqrt_sf128` 的運作機制,以及如何對應到 RISC-V CPU 模擬器中 ### 在 `root-riscv64.cfg` 設定檔中,有 `bios: "bbl64.bin"` 描述,這用意為何?提示:參閱 [Booting a RISC-V Linux Kernel](https://www.sifive.com/blog/all-aboard-part-6-booting-a-risc-v-linux-kernel) 根據提供的參考,`bbl` 全名為 Berkerly boot loader。它的運作如下 > ==bbl is expected to have been chain loaded from another boot loader==, with the entry point running in machine mode. ==It is passed a device tree from the prior boot loader stage==, and performs the following steps: > - ==The device tree that was passed in from the previous stage is read and filtered==. This allows bbl to strip out information that Linux shouldn't be interested in. > - ==`bbl` jumps to the start of its payload==, which in this case is Linux. 上面的重點已經大致標出,不過還是可以做個總結 `bbl64.bin` 作為 bootloader,會先將 device tree 的內容做過濾,將 linux kernel 載入記憶體並轉交控制權給 kernel。 ### 能否用 `buildroot` 編譯 Linux 核心呢?請務必參閱 [Buildroot Manual](https://buildroot.org/downloads/manual/manual.html) `buildroot` 有提供選項讓我們選擇是否編譯 linux kernel。進到設定畫面中的 **Kernel** 選項中 ![](https://i.imgur.com/0Rwlh8T.png) 若選定 `Linux Kernel` 選項,則可以進一步的做設定,如指定 kernel 的版本,從哪裡取的 kernel 原始庫,kernel dot-config 檔案等等。 ### 核心啟動的參數 `console=hvc0 root=/dev/vda rw` 代表什麼意思呢?這對應到模擬器內部設計的哪些部分? 核心啟動參數(kernel command line)常被用在讓開發者傳遞一些訊息給 kernel,在系統初始化期間做一些適當的客製化。可傳遞給 kernel 的參數很多,可以查閱 kernel 專案下的 `Documentation/admin-guide/kernel-parameters.txt` 得到每個參數的說明。 查閱上述文件得知 `console=hvc0 root=/dev/vda rw` | parameter | value | Description | | --- | --- | --- | | console | hvc0 | 指定 hypervisor console device 為系統的 output console device | | root | /dev/vda | 指定 /dev/vda 為 root file system | | rw | | 以可讀可寫的權限掛載 root file system | 對應到 `tinyemu` 系統內部,kernel command line 是寫在 Device tree 中,而 kernel 透過由 bootloader 所傳遞 Device tree 在 memory 中的位址,就可以去解析 device tree 的內容。 以下看在 `tinyemu` 中模擬 `riscv64` 機器的 Device tree 內容。 ```clike /* riscv_machine.c */ static int riscv_build_fdt(RISCVMachine *m, uint8_t *dst, uint64_t kernel_start, uint64_t kernel_size, const char *cmd_line) { FDTState *s; /* ... */ s = fdt_init(); /* ... */ fdt_begin_node(s, "chosen"); fdt_prop_str(s, "bootargs", cmd_line ? cmd_line : ""); if (kernel_size > 0) { fdt_prop_tab_u64(s, "riscv,kernel-start", kernel_start); fdt_prop_tab_u64(s, "riscv,kernel-end", kernel_start + kernel_size); } fdt_end_node(s); /* chosen */ fdt_end_node(s); /* / */ size = fdt_output(s, dst); /* ... */ fdt_end(s); return size; } ``` 上面看到,如果有定義 `cmd_line`,則會將 `cmd_line` 的內容存放在 `chosen` node 裡面的 `bootargs` property 裡面。 根據 [Device tree specification](https://www.devicetree.org/downloads/devicetree-specification-v0.1-20160524.pdf) 所定義的內容 ![](https://i.imgur.com/xIPCVw2.png) `bootargs` 的定義如下 > ==A string that specifies the boot arguments for the client program==. The value could potentially be a null string if no boot arguments are required. ### 為何需要在 host 端準備 `e2fsprogs` 工具呢?具體作用為何呢? 根據 Wiki 上的解釋,`e2fsprogs` 是關於操作管理 `ext2`, `ext3`, `ext4` 檔案系統的工具程式 > ==e2fsprogs (sometimes called the e2fs programs) is a set of utilities for maintaining the ext2, ext3 and ext4 file systems.== Since those file systems are often the default for Linux distributions, it is commonly considered to be essential software. 且查看 `e2fsprogs` 在 buildroot 中存放的位置。 ![](https://i.imgur.com/97Eon0t.png) 可以看到它放在 `build/busybox-1.30.1` 目錄下,所以 `e2fsprogs` 是被納入 `busybox` 工具程式中。 ### root file system 在 Linux 核心的存在意義為何?而 initramfs 的存在的考量為何? 系統的初始化和 root file system 有很大的關係。kernel 初始化的最後部分,會在 root file system 中找尋相關的 init 程式,所以以下這段程式碼被執行前,root file system 就已經要被掛載,且 init 相關程式要放在對的位置。 ```clike= /* init/main.c */ static int __ref kernel_init(void *unused) { // ... if (execute_command) { ret = run_init_process(execute_command); if (!ret) return 0; panic("Requested init %s failed (error %d).", execute_command, ret); } if (!try_to_run_init_process("/sbin/init") || !try_to_run_init_process("/etc/init") || !try_to_run_init_process("/bin/init") || !try_to_run_init_process("/bin/sh")) return 0; panic("No working init found. Try passing init= option to kernel. " "See Linux Documentation/admin-guide/init.rst for guidance."); } ``` 上面提到這段程式碼被執行前,root file system 就要被掛載(是廢話,因為 init 程式在 root file system 上),不過在真正的 root file system 被掛載前,可能會有其他暫時的 root file system 被掛載並且用來做早期的系統啟動,而這暫時性的 root file system 叫做 Initial RAM disk。**其存在的原因大部分是為了提前載入某些驅動程式**。例如,在 initial ram disk 中先載入 Ext4 的驅動,之後在掛載真正的 root file system 才能掛載。實作 initial ram disk 概念的方法有`initrd`(舊方法) 和 `initramfs`(新方法)。 :::info 可以參閱 linux 專案下的 `Documentation/filesystems/ramfs-rootfs-initramfs.txt` 檔案得到關於 `initramfs` ::: ### `busybox` 這樣的工具有何作用?請搭配原始程式碼解說 (提示: 參見 [取得 GNU/Linux 行程的執行檔路徑](http://blog.linux.org.tw/~jserv/archives/002041.html)) Busybox 在嵌入式系統領域是一個很受歡迎的工具程式。Busybox 易於設定,編譯,和使用。它成功的將一些常用的 utility program 整合成一個執行檔(將一些常用程式碼片段與多個工具程式分享,達到避免重複的程式碼片段),大大縮小硬體空間需求。 Busybox 是一個模組化的專案,可以透過類似於 linux kernl 提供的輔助工具程式,設定並客製化我們自己的 Busybox。 以下假設一個系統使用 `busybox`,他的 root file system 如以下所示 ``` $ tree . |-- bin | |-- ash -> busybox | |-- busybox | |-- cat -> busybox | |-- cp -> busybox | |-- ... | '-- zcat -> busybox |-- linuxrc -> bin/busybox |-- sbin | |-- init -> ../bin/busybox ... ``` 大部份的 utility program 都是 symlink 且都指向 `/bin/busybox`。所以執行 `ls`, `cp`, `cat` 等等本質上都是在執行 busybox。然而 busybox 必須知道要執行哪個功能,而這項資訊就是來自 `argv[0]`。 因為我們知道 `argv` 儲存了 command line 的資訊。在執行程式時 `argv[0]` 多為程式的名稱。 ``` $ ls -l # argv[0] => ls $ cat file.txt # argv[0] => cat ``` busybox 根據 `argv[0]` 就可以知道要執行什麼樣的任務。 ###### tags: `sysprog2019`