# 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`