Skip to main content

5 posts tagged with "3ds"

View All Tags

· 2 min read
Wei Ji

從影片逐幀取出畫面

這個步驟不難,使用 FFmpeg 只要一行指令就能完成:

mkdir output
ffmpeg -i bad_apple.mp4 -vf fps=5 output/%04d.png

將畫面轉換成 ASCII 純文字檔

這個步驟也不難,把剛剛生成的畫面載入、轉換再吐出去,搞定。

import fs from 'fs';
import asciify from 'asciify-image';

fs.mkdirSync('data', { recursive: true });

var options = {
fit: 'none',
width: 50,
height: 30,
c_ratio: 1,
color: false,
}

/**
* @param {string} path
* @returns {Promise<string>}
*/
const imageToAsciiPromise = (path) => new Promise((resolve, reject) => {
asciify(path, options, (err, asciified) => {
if (err) {
reject(err)
}
resolve(asciified.replaceAll(';', ' '));
});
});

const _files = fs.readdirSync('img');

const files = _files.map((file, index) => {
const sourcePath = (`img/${file}`);
const targetPath = `data/${(index + 1).toString().padStart(4, '0')}.txt`;
return {
sourcePath, targetPath
};
});

for (const { sourcePath, targetPath } of files) {
const acsii = await imageToAsciiPromise(sourcePath);
fs.writeFileSync(targetPath, acsii);
console.log(sourcePath)
}

播放

import * as os from "os";
import * as std from "std";

let count = 1;

const drawFrame = () => {
const timer = os.setTimeout(drawFrame, 200);
count++;

const f = std.loadFile(`data/${count.toString().padStart(4, '0')}.txt`);
if (f === null) {
os.clearTimeout(timer);
return;
}

std.out.puts('\x1B[2J');
std.out.puts(f + '\n');
}

drawFrame();

需要特別注意的是,這個執行環境是 QuickJs,osstd 都是該直譯器提供的模組。

· 12 min read
Wei Ji

本文以敘述的方式紀錄我在這個專案上的過程,並省略技術細節。若對實作細節有興趣,以下是以技術筆記的形式紀錄的文章:

前情提要

最近 (2022-11-06) 從天上掉下來的一台 3DS (?)

老實說對於一般過時 N 年的消費性電子產品,我是完全沒興趣多花時間去玩它,根據以往的經驗,hack 這種東西多半吃力不討好;花費大量的時間也只能獲得相對於現代設備少得可憐的算力。 不過個人對於這種「有機械輸入的橫式手持行動裝置」有著莫名憧憬,隨手搜尋看看刷機的資料,發現資源蠻多的,教學也很詳細,就想著來刷刷看。

閃亮亮症候群再次發作

跟朋友表示想在 3DS 上刷 Linux 之後,「工程師的成就清單」的話題就這樣被打開了,對話中除了提 「在非常見或非通用功能電腦上安裝 Linux」還提到了「在非常見的顯示器上播放 Bad Apple」。

過幾天 (2022-11-09),我便利用下班之餘試著在 3DS 上刷 Linux。刷機的第一部就是透過系統漏洞刷入 CFW (Custom firmware),讓 3DS 能夠從 SD Card 讀取 bootloader 開機,因為教學很詳細,照著步驟做沒有花太多時間就成功刷機了,當中有些教學性質的步驟也讓我看了 3DS Linux 的頁面之後就知道大概要做什麼,也成功的在 3DS 上進入 Linux 了。

到目前為止都出乎我意料之外的順利,於是我想:

我都有一台跑著 Linux 的 3DS 了,寫個程式讓它跑 Bad Apple 應該不難吧?

接著我又想:

如果能用我熟悉的 Javascript 寫程式就好了

於是「在 3DS 上跑 Linux 跑 Javascript 顯示 Bad Apple」就變成一個 side project 的目標了,接著這個 side project 就一發不可收拾了。

這個 Linux 是金魚腦

試著新增檔案並寫入一些資料,重開機之後卻發現檔案消失了,後來才了解到整個檔案樹是放在啟動 Linux 用的映像檔裡面,那些資料夾在開機之後是掛在 Ram Disk 上 1,自然沒有儲存的功能。

解決的辦法就是把資料存在 SD 卡上,但是這個 Linux 預設並不會掛載 SD 卡,研究了一會兒才知道它在 /dev/vda1,但是每次開機都要打一次指令顯得有點麻煩(提醒,3DS 沒辦法外接鍵盤,我只能用觸控筆敲指令)。

寫個腳本讓它在開機的時候執行便理所當然的變成必須完成的任務,然而它並沒有 systemd, rc.local 這類高級的東西。/etc/init.d/rcS 承擔 start 的功能 /etc/init.d/rcK 承擔 kill(stop) 的功能,會依序執行 /etc/init.d/rcS/S(\d\d.*)

腳本倒是容易搞定,但是我要怎麼把它包進去開機用的 img 裡面?建置 3DS Linux 的過程涉及一個 rootfs.cpio.gz 檔案,我必須把它解包之後加入我要的修改再包回去。

花了一點時間搞懂上述這些東西,才終於讓我的 Linux 開機會自動掛載 SD 卡 (2022-11-12)。

第一個挫折

依照自己的以往經驗:「要在 Host 上跑 Javascript 就先裝 Node.js」,然而最新版的 Node.js 並沒有 Armv6 的 prebuild release,不過一直到 11 版,還是有官方版的 armv6 prebuild 可以下載。下載之後在 3DS 上執行會拋出 -sh: ./node: not found 的錯誤,原因是官方的 prebuild 是仰賴 glibc,但是 3DS Linux 使用的是 musl libc。好吧,這個問題我也不是太陌生,之前在包 Docker 接觸過 Alpine Linux,所以對這類問題有個概念。

好唄,自己 build 就是了

雖然網路上找了一些 build 參數,但是怎麼試怎麼有問題,畢竟我平常又不寫 C,對專案的編譯配置不熟悉。後來找到一個叫做 Dockcross 的工具專門用來處理交叉編譯 (Cross Compiling)。但是相對舊版的 Node.js 仰賴 Python 2.X,Dockcross 的 Image 只有 3.X,這時我才知道 Node.js 的編譯過程居然仰賴 Python,我內心 OS:「SHAME, 程式語言之恥,Node.js 你作為一個直譯語言的直譯器的編譯過程居然仰賴另外一種直譯語言」。

好加在 Dockcross 允許擴充 Docker image 以符合專案需要,然而事情並沒有這麼簡單。Node.js 的編譯過程仰賴了編譯的產出物,也就是當我指定了編譯目標為 linux-armv6-musl ,會生成一些 tool,接著在編譯出 node 以前,它會在 host 呼叫這些 tool,但是我的 host 不是 linux-armv6-musl;於是拋出錯誤,編譯中止。(#╯O皿O)╯┻━┻

接著我發現在建置 3DS Linux 過程中使用的工具:buildroot 其實提供了一個界面可以勾選 node,而它的實作方式就是先編譯一份 x86 的版本,再跑 arm 的編譯,而編譯過程中用到的 tool 就回去 call x86 的版本。經過一波三折我終於弄出了「musl-armv6 版本的 Node.js v14」,然而問題並沒有因此解決,不然段落標題就不會是「第一個挫折」了。

$ node --version
v14.18.3

$ node -h
...
Process node (pid: 153, stack limit = 0xc6135dc6)
...
[<c0111610>] (v6_coherent_kern_range) from [<c01094c0>] (arm_syscall+0x15c/0x26c)
[<c01094c0>] (arm_syscall) from [<c0100060>] (res_fast_syscall+0x0/0x58)
Exception stack (0xc15elfa8 to 0xc15elff0)

總之 Node.js 對於那個年代的 3DS 來說 too powerful 了。(2022-11-13)

作為 ECMAScript 的信徒,這個挫折讓我很不甘心。

如果能用 node.js 跑的話感覺真的滿酷的,用 C 或 shell 就感覺很一般

並且正如我朋友說的,雖然我也不是不會寫 Shell 或是 C,但是感覺這樣就太無聊了(?)既然都難得要解 Bad Apple 的成就了,當然是加點料比較有趣,而且作為網頁前端工程師,窩真的不是很想用 Shell 或是 C 寫啊 _(:3」∠)_。誰知道 Node.js 毫無反應,就只是一團 stack 直接死在 Armv6 上。

碰巧那天 (2022-11-13) 跟朋友聊天的時候有提到在 Javascript 的圈子有個叫做「包」的東西,後來趁著放假整理瀏覽器書籤的時候發現 Bun 相關的東西,稍微查一下資料才想起來除了 Node.js 之外還有 Deno 跟 Bun 兩個實作 Javascript 的專案。

Bun 因為是後起之秀,Deno 又是用 Rust 寫的,具我所知 Rust 也能拿來寫嵌入式韌體,移植到 3DS Linux 的機率比較高一點,於是我便朝著這個方向去研究。

雖然 Deno 本身有一些很酷的 feature,包含交叉編譯的功能,可以把 Javascript 編譯成目標平台的執行檔。但是它本身卻沒有 arm linux 的 prebuild。就算自己跑編譯也會在下載仰賴套件 rusty_v8 的時候失敗,加上官方關於 32 bit ARM 支援的 issue 仍然開著。這些都還不包括 musl libc 可能會遇到的問題。看來此路仍然不通。我甚至一度產生了「乾脆用 Rust 寫吧,順便學一下這個我有興趣但是保持觀望一段時間的語言」的念頭。(2022-11-18)

等等,既然 Javascript 都有諸如 Node.js, Bun, Deno 這些不同的實作,甚至使用不同的 runtime 引擎(V8, JavaScriptCore),ECMAScript 單純作為標準,它的實作應該不只這樣吧?我也應該不是第一個想在嵌入式系統跑 Javascript 的人吧?

「Any application that can be written in JavaScript, will eventually be written in JavaScript.」

事實證明我不孤單:

一開始我嘗試了 tiny-js,因為在問答串的分數也相對高,它看起來足夠精簡,背後又是 Google。然而很快又遇到了 Musl libc 的問題,透過 musl-cross-make 這個工具沒有花多久就排除問題了,但是執行後直接跳 Segmentation fault,畢竟是個年久失修的專案,會這樣似乎也不怎麼意外。

後來我在這裡發現 quickjs 不只有中文資源,README 也有提到可以直接執行而不是只有 lib,相較於 jerryscript 的 README 沒有明講是 lib 還是可以直接跑,又有仰賴 Python 編譯,我就先選擇了 quickjs 嘗試。(對,我就歧視 Python,OHO)

它跟 Node.js 一樣,編譯過程仰賴自己編譯的 tool (qjsc),一樣要 build 兩次來解決,因為有前面的經驗,沒有花太久就把問題排除並成功在 3DS Linux 上跑起來了。 (2022-11-20)

Voila!

Javascript 能夠跑起來,實作的部份就簡單了(?)

  1. 用 FFmpeg 對影片以 5 FPS 取樣轉成圖片(原本試過 10 FPS,無奈設備太老舊跟不上)
  2. 把圖片轉成 ASCII 文字檔
  3. 用 Javascript 讀擋然後 print 出來

因為我現在手上同時有 arm 跟 x86 版本的 qjs 直譯器,改 Javascript 程式執行 debug 的速度就很快了,不用重新編譯,只要在 dev 環境測試到沒問題直接複製到 3DS 去再做第二次測試。

(縮圖有影片連結)


創用 CC 授權條款
Wei Ji以創用CC 姓名標示-相同方式分享 4.0 國際 授權條款釋出。

Footnotes

  1. https://github.com/linux-3ds/linux/wiki

· 4 min read
Wei Ji

前情提要

我已經成功的完成了 3DS 刷機並且在它上面跑起了 Linux,作為一個網頁前端工程師,我一點也不想為了在上面跑點東西跑去寫 C,而是想把我的舒適圈帶進去,也就是我要弄一個 Javascript runtime 進去 3DS Linux。

然而 3DS Linux 算是一個相對罕見的環境:Armv6 的硬體以及使用 musl libc,主流的 Javascript runtime 並沒有提供官方版本的 pre-build release,而是需要自己想辦法編譯。

musl libc

C 語言程式的執行其實仰賴 libc (C Run-Time Library),並且 libc 的實作不只一種,musl libc 便是其中的一種,而且被不少 Linux 發行版作為 C 標準函式庫。

交叉編譯 (Cross Compilation)

https://preshing.com/images/gcc-cross-compiler.png

交叉編譯就是先在 host 編譯出交叉編譯器 (cross compiler),再用這個交叉編譯器去編譯你的程式,最終目的是你的程式能夠跑在 target 上。

另外一個方法就是直接在 target 上跑編譯,但是 target 有可能是性能很低的晶片,這種方法效率非常差甚至無法完成(沒有足夠的記憶體完成編譯之類的),交叉編譯就是為了解決這種問題而存在的。

QuickJs

眾所皆知 ECMAScript 是一個語言的規範,而不是實作,因此遵守該規範的直譯器 (runtime) 其實有百百種。除了 Node.js 之外,Javascript runtime 還有後起之秀 DenoBun,而引擎 (engnine) 自然也不少,被各種瀏覽器、 Node.js 以及 Deno 使用的 V8 或是被 Bun 使用的 JavaScriptCore,除了這些之外還有諸多的實作。

QuickJS 是一個嬌小且可嵌入式的 Javascript 引擎。它支援 ES2020 規範,包含模組、非同步 Generator、Proxy、BigInt。它是以 C 語言撰寫並且沒有仰賴其他函式酷。

編譯 Cross Compiler

已經有人準備了方便的工具 musl-cross-make 讓我們能簡單的製作交叉編譯器。

首先下載這個 repo:

git clone git@github.com:richfelker/musl-cross-make.git
cd musl-cross-make

新增 config.mak 檔案:

touch config.mak

加入以下內容:

TARGET = arm-linux-musleabihf
COMMON_CONFIG += CFLAGS="-g0 -Os" CXXFLAGS="-g0 -Os" LDFLAGS="-s"
GCC_CONFIG += --with-arch=armv6 --with-mode=arm --with-fpu=vfp

執行編譯:

make -j ${nproc}

安裝(輸出執行檔),預設會輸出到專案目錄下的 output/ 資料夾:

make install

複製到 home

touch ~/.musl-cross-make
cp output/* ~/.musl-cross-make/.

把環境變數加到 .bashrc

export MUSL_CROSS_INSTALL="$HOME/.musl-cross-make" 
export PATH="$MUSL_CROSS_INSTALL/bin:$PATH"

編譯 QuickJs

下載 quickjs

git clone https://github.com/bellard/quickjs
cd quickjs

需要更動 Makefile 的一些內容:

新增 CROSS_PREFIX=arm-linux-musleabihf-

  CROSS_PREFIX=arm-linux-musleabihf-
HOST_CC=gcc
CC=$(CROSS_PREFIX)gcc
LIBS+=-ldl -lpthread          #找到這行
LIBS+=-ldl -lpthread -latomic #增加參數

接著跑編譯:

make -j ${nproc}

檢查編譯結果是不是符合我們的目標:

$ file qjs
qjs: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-armhf.so.1, with debug_info, not stripped

創用 CC 授權條款
Wei Ji以創用CC 姓名標示-相同方式分享 4.0 國際 授權條款釋出。

· 3 min read
Wei Ji

關於刷機流程,已經有非常詳盡的教學,並且操作細節跟跟當前 3DS 版本或是機型會有一些落差, 繁瑣過得流程我就不在本文一一介紹。

刷 CFW (Custom firmware)

不論型號,第一步就是刷 CFW,如此一來我們就能透過 CFW 作為 bootloader 去載入其他 bootloader 程式。

根據不同的系統版本,能夠利用的漏洞會有所差異,而本人持有的 3DS 處於 5.x.x 版的狀態,因此選擇 soundhax 來安裝 boot9strap

Luma3DS

Luma3DS 是一個系統補丁,允許使用者獲得更多權限或功能,並且能夠安裝非官方的 3DS 應用程式。

並且它能夠進行 chainload,也就是 boot9strap 引導到 Luma3DS;Luma3DS 再引導到其他韌體,如:Linux。

在「完成安裝」的步驟中,包含了安裝 Luma3DS、更新系統、安裝第三方應用程式等操作。

名詞註解

boot9strap

boot9strap 是一個漏洞利用工具,效果是能夠使 3DS 載入並執行第三方的程式。

Boot9 (ARM9 BootROM) 是任天堂在晶片內燒錄的韌體,它會對載入的韌體進行加密驗證,來確保韌體來自任天堂官方,然而這個算法存在漏洞,允許第三方韌體欺騙 BootROM 並被載入到 3DS 中執行獲得控制權。該漏洞由 derrekr 在 33C3 發表1。 網路上有中文的資料對漏洞的具體技術細節做更進一步的解釋,有興趣的朋友可以參考看看。

並且更進一步的 boot9strap 透過 NDMA 漏洞對受保護的 Boot9 記憶體位址寫入資料,從而執行其他程式2

33C3

全名是 「The 33rd Chaos Communication Congress」,總之就是一個非常大的駭客年會。3

NDMA (DSi New DMA)

暫存器的名稱。45

Footnotes

  1. Nintendo Hacking 2016 - Game Over 33C3. derrekr. Retrieved 2022-11-25, from https://derrekr.github.io/3ds/33c3

  2. sighax and boot9strap. SciresM. Retrieved 2022-11-25, from https://sciresm.github.io/33-and-a-half-c3/

  3. 33c3 intro | Just for noting. (n.d.). Retrieved 2022-11-25, from https://blog.m157q.tw/posts/2016/12/27/33c3-0/

  4. GBATEK - GBA/NDS Technical Info. (n.d.). Retrieved 2022-11-25, from https://problemkaputt.de/gbatek.htm#dsinewdmandma

  5. Super NES Programming/DMA tutorial - Wikibooks, open books for an open world. (n.d.). Retrieved 2022-11-25, from https://en.wikibooks.org/wiki/Super_NES_Programming/DMA_tutorial#DMA_registers

· 7 min read
Wei Ji

刷 Linux

如果已經刷完 CFW,並且安裝 Luma3DS 的話,在 3DS 上跑起來並不會太困難,只要照著指示下載檔案並放到 SD 卡內,接著從 Luma3DS 的 ChainLoader 開機就能成功進入 Linux 了。使用者為 root ,密碼為 toor

然而這個 Linux 的檔案系統是跑在 Ram Disk 上,並且預設不會掛載記憶卡,因此在系統內進行任何修改在開機之後都會消失。要把檔案弄進去的方法有兩種:

  1. 將檔案放到 SD 卡內,並掛載 /dev/vda1
  2. 將檔案放到開機映像檔 zImage

其實單論我的目的是在 Linux 跑一個 Jvascript 程式,大可直接用掛載的方式處理,但是使用 3DS Linux 的觸控板打指令真的是一件頗麻煩的事情(包含掛載指令),如果可以預載並自動執行一些腳本免除我手動輸入的步驟是再好不過了。

製作開機映像檔

在處理自動掛載以前,我們要知道如何把東西放進 zImage 內,這過程涉及兩個步驟:

  1. 產生 rootfs.cpio.gz
  2. 製作 zImage

產生 rootfs.cpio.gz

cpio 是 UNIX 作業系統的一個檔案備份程式及檔案格式1。cpio 是一個比較古老的備份命令,也是用於磁帶機備份的工具,雖然如此現在許多時侯仍然需要使用這命令,例如製作系統內存映像檔時等。像是系統映像檔通常位於 /boot 中,文件名以 initrd 開頭。2

因為它是基於磁帶的備份工具,因此無法直接替換掉該檔案裡面的特定文件,必須要把整個 cpio 解包進行編輯後再包回去。

linux-3ds 的專案有提供一個樣板可以直接下載:

$ wget https://github.com/linux-3ds/buildroot/releases/download/latest/rootfs.cpio.gz

或是你也可以從 buildroot 自己 build ,Buildroot 是一個可高度客製化的嵌入式 Linux 映像檔生成工具。Buildroot很強大,也很容易上手使用。3 Linux 3DS 專案下有一個已經與先準備好相關設定的 buildroot fork,照著指示做一樣很快就能生成 cpio 檔了。

cpio 作為 initrd 的格式,而 initrd 的英文含義是 boot loader initialized RAM disk,就是由 boot loader 初始化的 RAM Disk。在 linux 核心啟動前, boot loader 會將儲存介質中的 initrd 檔案載入到記憶體,核心啟動時會在訪問真正的根檔案系統前先訪問該記憶體中的 initrd 檔案系統。在 boot loader 組態了 initrd 的情況下,核心啟動被分成了兩個階段,第一階段先執行 initrd檔案系統中的"某個檔案",完成載入驅動模組等任務,第二階段才會執行真正的根檔案系統中的 /sbin/init 處理程序。4

編輯 cpio

我寫了一個小腳本來處理解包、覆蓋、包回去的步驟, 只要把檔案放在 original/rootfs.cpio, 要覆蓋的檔案樹放在 override/ 下。

#!/bin/bash

EXTRACT_TEMPD=$(mktemp -d)
OUTPUT_TEMPD=$(mktemp -d)
WORK_PATH=$(pwd)

if [ ! -e "$EXTRACT_TEMPD" ]; then
>&2 echo "Failed to create temp directory"
exit 1
fi

echo Extract Path: ${EXTRACT_TEMPD}
echo Output Path: ${OUTPUT_TEMPD}

# Extract and Archive again, /dev/console would missing
mkdir ${EXTRACT_TEMPD}/root
cd ${EXTRACT_TEMPD}/root

cpio -idv < "${WORK_PATH}/original/rootfs.cpio" 2> ${EXTRACT_TEMPD}/log.txt
cp -rf "${WORK_PATH}"/override/* ./

#find . > ${OUTPUT_TEMPD}/log-1.txt
find . | cpio -ov -H newc > ${OUTPUT_TEMPD}/rootfs.cpio 2> ${OUTPUT_TEMPD}/log.txt

cd ${OUTPUT_TEMPD}
gzip -c rootfs.cpio > rootfs.cpio.gz

chown <Normal-User> -R ${OUTPUT_TEMPD}
chgrp <Normal-User> -R ${OUTPUT_TEMPD}

chown <Normal-User> -R ${EXTRACT_TEMPD}
chgrp <Normal-User> -R ${EXTRACT_TEMPD}

比如,編譯出來的程式會仰賴 /usr/lib/ld-linux-armhf.so.3,但是 linux 3ds 提供的 cpio 並沒有這個路徑,就可以在 override/ 下加入:

./usr/lib:
ld-linux-armhf.so.3 -> ../lib/libc.so

記得執行操作的時候要有 root 權限,因為 rootfs.cpio 裡面有些檔案類型需要 root 才能新增。

製作 zImage

照著指引安裝仰賴的函式庫、clone repo、把 rootfs.cpio.gz 放到專案目錄下,接著執行:

$ $ ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- make -j$(nproc) nintendo3ds_defconfig all

它就會 build 出包含 zImage 在內,在 3DS 上跑起 Linux 需要的一些檔案。

自動掛載

這個 Linux 並沒有諸如 systemd, rc.local 之類的高級玩意,倒是有這些東西:

$ ls /etc/init.d
rcK rcS S01syslogd S02klogd S02sysctl

/etc/init.d/rcS 承擔 start 的功能 /etc/init.d/rcK 承擔 kill(stop) 的功能,會依序執行 /etc/init.d/rcS/S(\d\d.*),很好理解。

於是我就做了一個 /etc/init.d/S03sdmnt 來自動掛載 SD 卡:

#!/bin/sh

case "$1" in
start)
echo "Mount SD card"
# Mount SD card
mkdir /mnt/sdcard
mount /dev/vda1 /mnt/sdcard
;;
stop)
echo "Unmount SD card"
# Todo
;;
*)
echo "Usage: /etc/init.d/S03sdmnt {start|stop}"
exit 1
;;
esac

exit 0

小結

研究到這裡,會發現 Chain Loading5 的影子非常明顯: 刷進去的 boot9strap 會 boot Luma3DS; Luma3DS 會從 luma/payloads/ boot firm_linux_loader.firmfirm_linux_loader.firm 再去 boot arm9linuxfw.binarm9linuxfw.bin 再用 zImage 開機(?)


創用 CC 授權條款
Wei Ji以創用CC 姓名標示-相同方式分享 4.0 國際 授權條款釋出。

Footnotes

  1. Cpio - 維基百科,自由的百科全書. Retrieved 2022-11-25, from https://zh.wikipedia.org/zh-tw/Cpio

  2. 會紅的 Linux 筆記: cpio指令. Retrieved 2022-11-25, from http://canred.blogspot.com/2013/04/cpio.html

  3. 精通嵌入式Linux 第三章:Buildroot – LotLab. Retrieved 2022-11-25, from https://www.lotlab.org/2020/04/08/mastering-embedded-linux-part-3-buildroot/

  4. 【Linux技術】Linux核心Initrd機制解析,核心更新步驟,grub組態說明-阿里雲開發者社區. Retrieved 2022-11-25, from https://developer.aliyun.com/article/445357

  5. Chain loading - Wikipedia. Retrieved 2022-11-25, from https://en.wikipedia.org/wiki/Chain_loading