Wednesday, 17 February 2016

buildRoot study - 建立自己的作業系統


  • Introduction
因為想要了解整個Linux作業系統,從開機到系統穩定的中間流程,到底經歷過哪幾個步驟,
所以想要自己兜「boot-loader」,kernelroot file system
研究過程中發現了,原來早已經有組織在從事類似的專案,像是「Buildroot」,「Yocto/OpenEmbedded」
所以我就想來研究一下Buildroot的架構。


這篇文章的成果會從Buildroot專案中build出「bootloader」, 「kernel」和「root fileSystem」,
然後我們會在將這幾個元件和官方的firmware放到raspberry pi 2裏面開機後來研究整個開機流程。


以一個專案標語:Making Embedded Linux Easy啟動的這個專案,
基本的概念就是先建立一個自用的cross-compilation toolchain,
然後經由這個這個cross-compiler去編譯kernel和建立root file system。

主要的設計理念是:
  • Simple to use
  • Simple to customize
  • Reproducible builds
  • Small root filesystem
  • Relatively fast boot
  • Easy to understand

那有誰在用呢?
  • System makers
  • Google
  • Barco
  • Rockwell Collins

  • Processor vendors
  • Imagination Technologies
  • Marvell
  • Atmel
  • Analog Devices

當然除了以上大公司,還有很多其他公司和業餘愛好者都會用它來開發版子。

  • Simplified Linux system architecture
一個簡單的Linux系統架構,如下圖,主要分為4個部份:
1.  Hardware
2. Boot loader
3. Linux Kernel
4. User space (Applications)
除了Hardware以外,Buildroot專案建制過程中其他的3個部份都會自己建置。
所以建制完後,我們主要會有「ToolChain」, 「bootloader image」, 「kernel image」和「root fileSystem image」。




假設我們的專案現在已經建制好了,也已經放到儲存裝置(Flash or EMMC ....),電源插上去後,
第 1 個跑的程式就是BootLoader,他會啟動一些基本的驅動程式,以供開機中使用,最終就是將kerner載入到記憶體裡,接下來就是將控制權轉交給Kernel。
第2個就是Kernel會初始化硬體裝置和我們的檔案系統,然後執行第一個程式/sbin/init。
第3步init就會啟動剩下來user space的service和application。
第4步我們通常就會看到login shell展示再我們面前了,在後面章節會有更詳細的介紹。

  • Preparation
Ubuntu
這邊使用的Host OS是Ubuntu。

數莓派[廢話]


TTL Cable [天瓏書局有在賣]
   

Docker
使用一個完全乾淨的Container來做這件事,
事情會比較單純化,也會加深很多印象。
詳細Docker內容請參考網站

開啟Docker
  • sudo docker run -t -i -v ~/mnt/mnt_docker:/tmp/package ubuntu bash
~/mnt/mnt_docker : /tmp/package語法是host 和container的資料共享 => host folder : Container folder
接下來所有內容都是在Docker裏面執行的。

下載套件
  • sudo apt-get update
  • sudo apt-get install g++ make gawk -y
  • sudo apt-get install git-core libncurses5-dev vim -y
  • sudo apt-get install wget python unzip bc -y

  • Build buildRoot

最簡單的建置法
  • cd buildroot/
  • make raspberrypi2_defconfig
  • make 2>&1 | tee build.log
結束~~
但是我要故意把他複雜化,這樣才可以理解他裏面的運作原理!
請接下面的Build Step by Step開始。

Build Step by Step

  • get sources by git
  • cd /tmp/
  • cd buildroot/

  • get build information
buildroot裏面有很多內建的組態,

裏面有raspberrypi2!
在來看一下怎麼弄,很簡單
  • vim /board/raspberrypi2/readme.txt
官方github上的文件--> 請點我

Download all Sources 
buildroot的所有package, sources都是build之前才會從網路上下載下來,所以第一次build會很久,
因為包含下載,所以這邊我們先來研究一下,他下載了多少東西,請下指令:
  • make help

發現裏面有個指令是
 source                 - download all sources needed for offline-build
 下載所有資源之前要先建立組態
  •  make list-defconfigs
  •  
 上面這個指令會顯示目前有支援多少預設的組態,
 我們發現有數莓派2的,所以下以下指令:
  •  make raspberrypi2_defconfig
 然後只要下這個指令就會下載所有個資源:
  •  make source

套件說明:
總共下載了21個套件,下載完的套件都放在資料夾「dl」裏面,
底下就來介紹一下每個套件大概的用途:
  •  xz-5.2.2:
  •  包含對檔案的壓縮和解壓縮,裏面提供了「lzma」和新的「xz」壓縮的格式。
  • gcc-4.9 - 略
  • binutils-2.24 - 略
  • gmp-6.1.0 - 略
  • mpc-1.0.3 - 略
  • mpfr-3.1.3 - 略
  • linux-header - 略
  • m4-1.4.17:
  • 裏面包含了巨集處理器(macro processor)。
  • uClibc-ng-1.0.12
  • 是一種C的library,一般來說都是用Glibc,但是這邊選用uClibc,因為他比較小,也支援沒有MMU的架構,非常適合比較小的系統(< 10 MB)。
  • kmod-22:
  • 包含了讀取kernel module的函式庫和程式。
  • pkgconf-0.9.12:
  • 在設定組態或者在make 專案時,這個套件會傳遞一些include和library的路徑給建置的軟體。
  • busybox-1.24.1
  • Busybox在單一的可執行文件中提供了精簡的Unix工作集,可運行於多款POSIX環境得的操作系統。
  • dosfstools-3.0.28
  • 可以讓使用者在GNU/Linux OS上很快速的建立,設立標籤和檢查MS-DOS FAT 的檔案格式(mkfs.fat, fsck.fat and fatlabel)。
  • e2fsprogs-1.42.13:
  • 裏面有處理ext2 檔案系統的程式。當然他也支援ext3和ext4。
  • genext2fs-1.4.1
  • 這個程式可以讓你建立ext2的檔案系統。
  • genimage-8
  • 可從一個root filesystem裏面建立多種檔案系統和flash image。
  • confuse-2.8:
  • libcoufuse提供了一些組態分析函式庫。
  • flex-2.5.37:
  • 裏面的套件可以產生可辨識文字pattern的程式(反正就是正規表示法那些東西)。
  • mtools-4.0.18:
  • 是一套允許讓Unix系統去操控MS-DOS格式上的檔案操作,像是讀寫檔,和搬運檔案等等。
  • rpi-firmware:
  • 數莓派官方release出來的軔體,這部份待回會在說明。
  • fakeroot_1.20.2.orig
  • 可用來模擬root的權限去做一些操作。

修改組態
如果想要自己修改設定,就跟修改kernel 組態一樣,
這邊我們來修改一下兩個設定,請輸入
  • make menuconfig

然後將選項 Filesystem images ---> tar the root filesystem--->Compression method選擇是bzip2,如下圖:


這樣我們待回的root filesytem就會以tar ball 的型式。
然後我們也想要自己build個u-boot來玩玩。
所以選項 Bootloaders -->  選擇U-boot選項,Board defconfig 輸入「rpi_2」。
上面那個指令會產生一個.conifg檔案,這邊來研究一下他開啟了哪些功能-->請按我



開始建制
剛剛組態已經產生,sources也已經下載好了,接下來我們就開始建制。

  • make 2>&1 | tee build.log

所有的package都會解壓縮在output/build,並且建置。
所有的結果都會在output/images裏面。

在output/images裏面,
sdcard.img算是整個結果的image,
假設你的sdcard是 /dev/sdd,
你只要下指令:
  • sudo dd if=sdcard.img of=/dev/sdd

然後在把sdcard卡插進去pi2裏面,就可以開機了,
但是像我說的,我要故意把他複雜化,這樣才可以理解裏面的運作原理。

在output/images裏面,
我們待回會用到的是:
1. kernel                        --> zImage
2. root file system        --> rootfs.tar.bz2
3. device tree blob      --> bcm2709-rpi-2-b.dtb
4. raspberry firmware   --> rpi-firmware/*
5. boot loader               --> u-boot.bin

所以底下會有另外5相對應的標題,
教怎麼把以上5個部份裝起來,並且開機。

  • Partition SD Card
首先我們必須有一塊Micro SD,我們要將它分成兩個partition,其中第一個為boot section,另一個partition是放我們的rootfs的地方

  1. 假設路徑是/dev/sdd,先進入fdisk系統
  • $ sudo fdisk /dev/sdd

  1. 將預設單位改成cylinders
  • Command (m for help): u

  1. 建立一個新的DOS格式的partition table:
  • Command (m for help): o

  4. 建立放boot loader的partition
  (以下兩步的重點是規劃200MB的boot partition,其他的都規檔案系統用)
  • Command (m for help): n
  • Partition type:
  • p   primary (0 primary, 0 extended, 4 free)
  • e   extended
  • Select (default p): p    (The new partition is a primary partition.)
  • Partition number (1-4, default 1): (Press Enter for default.)
  • Using default value 1
  • First cylinder (2-15193, default 2): 2
  • Last cylinder, +cylinders or +size{K,M,G} (2-15193, default 15193): +200M

 5. 建立放檔案系統的partition
  • Command (m for help): n
  • Partition type:
  • p   primary (1 primary, 0 extended, 3 free)
  • e   extended
  • Select (default p): p    (The new partition is a primary partition.)
  • Partition number (2-4, default 2): (Press Enter for default.)
  • Using default value 2
  • First cylinder (2048-15193, default 2): 2048
  • Last cylinder, +cylinders or +size{K,M,G} (2-15193, default 15193): 15193

  6. 將第一個磁區標注為bootable 
  • Command (m for help): a
  • Partition number (1-4): 1   (Select Partition 1 to be active.)
  
  7. 將第一個磁區改為FAT32
  • Command (m for help): t
  • Selected partition 1
  • Hex code (type L to list codes): c 
  • Changed system type of partition 1 to c (W95 FAT32 (LBA))

8.寫入新的partition table
  • Command (m for help): w

9. 格式化新的boot load磁區為DOS FAT32 檔案系統
  • $sudo mkfs.vfat -F32 /dev/sdd1
  • mkfs.vfat 3.0.13 (30 Jun 2012)

10.格式化第二個磁區為ext4檔案系統
  • $sudo mkfs.ext4 /dev/sdd2

11. 掛載裝置
  • mkdir -p ~/mnt/fat32
  • mkdir -p ~/mnt/rtfs
  • sudo mount /dev/sdd1 ~/mnt/fat32
  • sudo mount /dev/sdd2 ~/mnt/rtfs

所以我們得到了兩個Partitions,概念如下:


  • 1. Kernel Image
我們將kernel的Image複製到boot section
  • sudo cp output/images/zImage ~/mnt/fat32


  • 2. root file system
將root file system的image複製到第二個partition底下然後再解壓縮。

  • sudo cp outpupt/images/rootfs.tar.bz2 ~/mnt/rtfs
  • cd ~/mnt/rtfs
  • sudo tar -jvxf rootfs.tar.bz2
  • sudo rm rootfs.tar.bz2 

所以我們的極小型root filesystem已經到位了。
這一個file system裏面所有的東西,基本上都是busybox去兜出來的。
由下圖可以看到,幾乎所有的檔案都是連結到/bin/busybox的。
而/dev這資料夾的檔案都是kernel devtmpfs所建立出來的。


  • 3. device tree blob
將裝置樹檔複製到boot section。
  • sudo cp output/images/bcm2709-rpi-2-b.dtb ~/mnt/fat32/


  • 4. raspberry pi official firmware

這些軔體並不是這個專案build出來的,是從官方那邊下載下來的。
其實有兩種方式取得:

第二個為Ra pi 2的image file,
如果想要把相對應的檔案取出來,請看底下3步驟
阿不想知道的,請忽略底下3步驟:
  1. 下載image  File
  1. 這個image有兩個partitions,所以無法直接掛載,需要先找到boot partition的開始section

由上可知道,第一個磁區是從8192開始,所以總共要偏移8192*512 = 4194304 bytes

3. mount 第一個磁區.
  •    $ sudo mount -o loop,offset=4194304 2015-05-05-raspbian-wheezy.img ~/mnt
  •    


在firmware裡,我們需要的檔案有:
start.elf (GPU frimware) 
bootcode.bin  ( bootloaders)
config.txt (裏面也是有一些組態)
cmdline.txt (這個檔案裏面的文字都會當作參數傳遞給Kernel)
fixup.dat  (用來組態GPU和CPU之間的SDRAM partition)

反正就是把rpi-firmware資料夾底下的東西都複製到boot section就對了
  • sudo cp output/images/rpi-firmware/* ~/mnt/fat32


  • 5. U-boot
將u-boot.bin複製到boot section,
  • sudo cp output/images/u-boot.bin ~/mnt/fat32

然後修改config.txt,
kernel=zImage 
改成
kernel=u-boot.bin

這樣在final-stage bootloader時,就會去讀我們的u-boot.bin而不是直接進入kernel。



  • 開機和驗收

1. 接上TTL Cable
2. 開啟終端機
  • sudo screen /dev/ttyUSB0 115200

ttyUSB0是我這邊的Port,每個人的不一定一樣,所以請根據自己的case修改(然後要記得裝driver),
bound rate我這邊是115200,因為這是rapi2的預設,如有其他case也請自行修改。

3. 接上電源
在rapi2上的HDMI接著螢幕的狀況下,接上電源
如果成功的話,你就會發現終端機上開始有log了,
記得在Hit any key to stop autoboot時,隨便敲個字,
然後就會進入U-boot的命令列了,如下:


4. 手動u-boot開機

我們第一次可以先手動開機,藉由一個指令一個指令去了解做了什麼事。
  • # Tell Linux that it is booting on a Raspberry Pi2
  • setenv machid 0x00000c42
  • # Set the kernel boot command line
  • setenv bootargs "earlyprintk console=tty0 console=ttyAMA0 root=/dev/mmcblk0p2 rootfstype=ext4 rootwait noinitrd"
  • # Save these changes to u-boot's environment
  • saveenv
  • # Load the existing Linux kernel into RAM
  • fatload mmc 0:1 ${kernel_addr_r} zImage
  • # Boot the kernel we have just loaded
  • bootz ${kernel_addr_r}

  • 如果不是在開發kernel或測試,又或者沒有遇到一些早期loader的問題的話,可以把earlyprintk這個選項省略,就不會一堆log了。
  • 我們要使用serial console所以這個選項要留著。
  • /dev/mmcblk0p2這個路徑是Raspbian的,如果路徑不一樣的話,這邊要記得改成存放rootfs的路徑。
  • 如果rootfs的格式不是EXT4的話,這邊要記得˙改。

當這一行bootz ${kernel_addr_r}執行下去後,如果沒問題的話,就會看到一堆log了,代表已經順利開機,u-boot已經順利將控制權交給kernel了。

但是跑到「random: nonblocking pool is initialized」這一行時,你會發現,
怪了,怎麼不會動了??而且HDMI上也沒有畫面。

5. 改回kernel開機
這時關機以後,我們在嘗試著修改config.txt,
把kernel=u-boot.bin
再改回
kernel=zImage

在TTL  cable還接著的狀態下,再次開機,
我們發現,HDMI下的螢幕,開機正常,
但是終端機還是卡在相同的一行,
所以我們對著pi2上的鍵盤輸入登入帳號:root
然後嘗試著關機輸入:halt
顯示圖如下:

6. 再次手動u-boot開機
再次修改config.txt,
把kernel=zImage
再改回
kernel=u-boot.bin

然後跟剛剛一樣的步驟,發現還是停在「random: nonblocking pool is initialized」,
這時候我們一樣對著接著rapi2的鍵盤(不是終端機)輸入:root 按Enter
然後再輸入:halt 按Enter
發現有log了,也正常關機了。
如下圖:

7. 檢查inittab
正常開機後,檢查一下/etc/inittab,
發現在開機階段,只有宣告tty1的getty:
# Put a getty on the serial port
tty1::respawn:/sbin/getty -L  tty1 0 vt100 # GENERIC_SERIAL

數莓派的UART driver是AMA,
所以我們自己在加入ttyAMA0的getty :
# Spawn a getty on Raspberry Pi serial line
ttyAMA0::respawn:/sbin/getty -L  ttyAMA0 115200 vt100 

然後reboot以後到log停住的地方,按一下Enter就會出現login shell了。









  • How the Raspberry Pi boots up

如上圖,pi 2 的SOC是採用Broadcom BCM2835,它包含了 ARM11的CPU,VideoCore GPU, ROM chips, SDRAM和其他的東西。

底下是搭配剛剛我們兜出來的元件概念圖,來解釋數莓派上簡單的Linux開機流程:

1. 當pi 2 通電以後,其實第一個啟動的指令,是在SOC的ROM上,好吧,所以代表這邊已經是黑箱了,無法繼續追下去=.=,所以這個黑箱裡所做的是是所謂的first-stage bootloader。在first-stage時,他會mount上我們的SD card裡的FAT32的 boot partition,接下來我們SD Card裏面的bootcode.bin 就是second-stage bootloader。根據文獻,在first-stage時,CPU跟RAM都還沒被初始化(意思是CPU還是在reset的狀態)。所以到目前為止,都是再GPU裏面運行,而不CPU。

2. 接下來bootloader.bin就會被讀到GPU上的L2 cache上並且被執行。在這步驟就會啟動RAM,並且讀取start.elf檔。

3. 讀取start.elf以後,就是third-stage bootloader了,這個檔案是GPU的軔體,他會去讀取再config.txt裏面得的設定(根據網路文獻,就是把config.txt當成BIOS setting就對了XD)。裏面有些參數是我們可以調整的,都是是些frequency,有需要可以參考[h]。在start.elf階段,GPU和CPU所使用的RAM還是在不同的區段(ex. 如果GPU使用0X0000F000~0X0000FFFF的話,CPU就會使用0X00000000~0X0000EFFF)。

4. 接下來,如果有cmdline.txt的話,在start.elf裏面也會被讀取,這個檔案包含了一些cmd的參數,然後跑fixup.dat,組態GPU和CPU之間的SDRAM partition,在這個階段CPU就會reset,也代表交接結束了。

5. 到這個階段已經是final-stage bootloader了,接下來start.elf會讀取u-boot.bin。

6. 然後我們再自己手動把kernel 也就是zImage給讀進記憶體裏面,並且將控制權交給kernel,然後作業系統就啟動了。
當作業系統啟動後,其實GPU還在運行,因為start.elf並不只是GPU的軔體,他也是一個"proprietary OS (私權OS)"叫作VideoCore OS,有時候正常的OS需要一些參數時,也會經由˙mailbox messaging system去要求VCOS傳給他。

7. 當Kernel將大部分的硬體裝置和我們的檔案系統初始化後,就會執行第一個程式/sbin/init。

8. 然後會在去啟動剩下來user space的service和application。在來就是login shell。

  • 結論
1. 正常的狀況下,buildroot並沒有提供GPIO的login shell。(因為是使用busybox,所以當kernel將控制權交給busy的init時,就沒有log了,這地方要再研究一下)(solved --> 要修改inittab

2. 如果是用u-boot去開機的話,螢幕的驅動程式都不會作用。但是鍵盤是可以的。(所以照裡來說,u-boot裏面應該要有驅動才對,這地方也要再研究一下)
Jserv:另一處「如果是用u-boot去開機的話,螢幕的驅動程式都不會作用」,應該明確說 HDMI output 是否運作,要注意到 RPi 的設定,這點在官方網頁就有說明了

3. 所以整個專案下來,我們已經了解了數莓派上Linux的開機流程,接下來,我要嘗試著不要依賴buildroot套件,而是每個元件都自己建立會更加的了解Linux的構成。
Jserv:「接下來,我要嘗試著不要依賴buildroot套件,而是每個元件都自己建立會更加的了解Linux的構成」這個目標也不壞,但為何不先試著修改 buildroot 呢?嘗試新增或移除特定的套件呢?
如果現在是 2000 年,我會鼓勵學生用「不透過 buildroot 一類的工具,徒手建立 root filesystem」,但我現在不會建議這樣作,原因是:(1) 你得跟得上時代,和技術社群用相似的開發工具、開發流程,知道如何和其他開發者交流; (2) 這幾年系統變異很大,諸如用 systemd 取代 init、Yocto/OpenEmbedded 技術社群的快速成長,幾乎只能透過閱讀 git log,才能窺知技術發展的動向。倘若你今天還是「閉門造車」,恐怕只是遠離這個世界

4. Jserv:至於修改 buildroot,你可以參考這個專案:https://github.com/mpolakovic/qemrdux-player # 透過 buildroot,從無到有打造一個 MP3 Player 的韌體


  • ref:

10 comments:

  1. 您好,很高興能夠閱讀這篇文章,受益良多。

    好奇想問您一個問題,在第四點 raspberry pi official firmware 的部分,您將 official image 中的
    start.elf (GPU frimware)
    bootcode.bin ( bootloaders)
    config.txt (裏面也是有一些組態)
    cmdline.txt (這個檔案裏面的文字都會當作參數傳遞給Kernel)
    fixup.dat (用來組態GPU和CPU之間的SDRAM partition)

    取出來,這和原來我用 buildroot 直接 make 後,用 dd 放進 SD 卡裡有什麼不同 ?

    換句話說,那原來 buildroot 裡面沒有包含這幾個檔案嗎 ??

    謝謝

    ReplyDelete
    Replies
    1. Hi Yu-Ting,
      其實Buildroot裡面他也是從官方那邊下載的,
      我在另一個研究中有發現這件事,如果你有興趣的話可以參考一下:
      https://www.gitbook.com/book/hugh712/raspberry_mp3/details

      如有問題在問我阿~大家可以交流 :)

      Delete
    2. 關鍵字是Package Infrastructures,
      然後你可以看一下路徑「buildroot/package/rpi-firmware」底下的檔案,
      你就會發現原來他也是用官方的韌體了 :)

      Delete
  2. Hi, 不好意思,想再問您一個問題,是關於從 u-boot 手動進入 Linux Kernel 的部分。

    當我輸入玩 bootz ${kernel_addr_r} 時,會卡住,不知道是否有遇到類似的問題呢 ?

    http://imgur.com/a/kOTeE

    謝謝

    ReplyDelete
    Replies
    1. Hi Yu-Ting,
      這個現象通常會很多種條件造成,
      不過我猜有可能是cmdline.txt沒有寫好,
      你可以先檢查一下你的cmdline.txt,
      又或者是參考一下這一篇:
      https://www.raspberrypi.org/forums/viewtopic.php?f=91&t=57158

      關鍵字:
      by jojopi » Wed Oct 02, 2013 8:51 pm

      Delete
  3. Hugh 你好,

    感謝你的好經驗分享! 如果有機會想認識你多交流,不知道怎麼連絡你比較好?

    Ryan

    ReplyDelete
    Replies
    1. Hi Ryan,

      可加FB,https://www.facebook.com/hugh.null

      Delete
  4. This comment has been removed by the author.

    ReplyDelete
  5. 想加個好友請教一些問題....謝謝

    ReplyDelete