綜合能力訓練:在樹莓派上動手寫一個小OS(2):實驗16-1:輸出welcome benos
本文節選自《奔跑吧linux核心 入門篇》第二版第16.2.1章
和笨叔一起玩樹莓派,動手寫BenOS!
實驗16-1:輸出“Welcome BenOS!”
1.實驗目的
1)瞭解和熟悉ARM64彙編。
2)瞭解和熟悉如何使用QEMU和GDB除錯裸機程式。
2.實驗要求
1)編寫一個裸機程式並在QEMU模擬器中執行,輸出“Welcome BenOS!”字串。
2)在樹莓派上執行編譯好的裸機程式。
3.實驗詳解
由於我們寫的是裸機程式,因此需要手動編寫Makefile和連結指令碼。
對於任何一種可執行程式,不論是.elf還是.exe檔案,都是由程式碼段.text、資料段.data、未初始化資料段.bss等段(section)組織的。連結指令碼最終會把一大堆編譯好的二進位制檔案(.o檔案)綜合生成為二進位制可執行檔案,也就是把所有二進位制檔案整合到一個大檔案中。這個大檔案由總體的.text/.data/.bss段描述。下面是本實驗中的一個連結檔案,名為link.ld。
1 SECTIONS
2 {
3 . =
0x0;
4 .text.boot : { *(.text.boot) }
5 .text : { *(.text) }
6 .rodata : { *(.rodata) }
7 .data : { *(.data) }
8 . = ALIGN(
0x8);
9 bss_begin = .;
10 .bss : { *(.bss*) }
11 bss_end = .;
12 }
在第1行中,SECTIONS是LS(Linker Script)語法中的關鍵命令,用來描述輸出檔案的記憶體佈局。SECTIONS命令告訴連結檔案如何把輸入檔案的段對映到輸出檔案的各個段,如何將輸入段合為輸出段,以及如何把輸出段放入程式地址空間(VMA)和程式地址空間(LMA)。SECTIONS命令格式如下。
SECTIONS
{
sections-command
sections-command
…
}
sections-command有4種。
-
ENTRY命令。
-
符號賦值語句。
-
輸出段的描述(output section description)。
-
段的疊加描述(overlay description)。
在第3行中,“.”非常關鍵,它代表位置計數(Location Counter,LC),這裡把.text段的連結地址被設定為0x0,這裡連結地址指的是裝載地址(load address)。
在第4行中,輸出檔案的.text.boot段內容由所有輸入檔案(其中的“
”可理解為所有的.o檔案,也就是二進位制檔案)的.text.boot段組成。在第5行中,輸出檔案的.text段內容由所有輸入檔案(其中的“”可理解為所有的.o檔案,也就是二進位制檔案)的.text段組成。
在第6行中,輸出檔案的,rodata段由所有輸入檔案的.rodata段組成。
在第7行中,輸出檔案的,data段由所有輸入檔案的.data段組成。
在第8行中,設定為按8個位元組對齊。
在第9~11行中,定義了一個.bss段。
因此,上述連結檔案定義瞭如下幾個段。
-
.text.boot段:啟動首先要執行的程式碼。
-
.text段:程式碼段。
-
.rodata段:只讀資料段。
-
.data段:資料段。
-
.bss段:包含初始化的或初始化為0的全域性變數和靜態變數。
下面開始編寫啟動用的彙編程式碼,將程式碼儲存為boot.S檔案。
1 #include
"mm.h"
2
3 .section
".text.boot"
4
5 .globl _start
6 _start:
7 mrs x0, mpidr_el1
8 and x0, x0,#
0xFF
9 cbz x0, master
10 b proc_hang
11
12 proc_hang:
13 b proc_hang
14
15 master:
16 adr x0, bss_begin
17 adr x1, bss_end
18 sub x1, x1, x0
19 bl memzero
20
21 mov sp, #LOW_MEMORY
22 bl start_kernel
23 b proc_hang
啟動用的彙編程式碼不長,下面作簡要分析。
在第3行中,把boot.S檔案編譯連結到.text.boot段中。我們可以在連結檔案link.ld中把.text.boot段連結到這個可執行檔案的開頭,這樣當程式執行時將從這個段開始執行。
在第5行中,_start為程式的入口點。
在第7行中,由於樹莓派有4個CPU核心,但是本實驗的裸機程式不希望4個CPU核心都執行起來,我們只想讓第一個CPU核心執行起來。mpidr_el1暫存器是表示處理器核心的編號 。
在第8行中,and指令為與操作。
第9行,cbz為比較並跳轉指令。如果x0暫存器的值為0,則跳轉到master標籤處。若x0暫存器的值為0,表示第1個CPU核心。其他CPU核心則跳轉到proc_hang標籤處。
在第12和13行,proc_hang標籤這裡是死迴圈。
在第15行,對於master標籤,只有第一個CPU核心才能執行到這裡。
在第16~19行,初始化.bss段。
在第21行中,設定sp棧指標,這裡指向記憶體的4 MB地址處。樹莓派至少有1 GB記憶體,我們這個裸機程式用不到那麼大的記憶體。
在第22行中,跳轉到C語言的start_kernel函式接下來需要跳轉到C語言的start_kernel函式,這裡最重要的一步是設定C語言執行環境,即堆疊。
總之,上述彙編程式碼還是比較簡單的,我們只做了3件事情。
-
只讓第一個CPU核心執行,讓其他CPU核心進入死迴圈。
-
初始化.bss段。
-
設定棧,跳轉到C語言入口。
接下來編寫C語言的start_kernel函式。本實驗的目的是輸出一條歡迎語句,因而這個函式的實現比較簡單。將程式碼儲存為kernel.c檔案。
#include
"mini_uart.h"
void start_kernel(
void)
{
uart_init();
uart_send_string(
"Welcome BenOS!\r\n");
while (
1) {
uart_send(uart_recv());
}
}
上述程式碼很簡單,主要操作是初始化串列埠和往串列埠裡輸出歡迎語句。
接下來實現一些簡單的串列埠驅動程式碼。樹莓派有兩個串列埠裝置。
-
PL011串列埠,在BCM2837晶片手冊中簡稱UART0,是一種全功能的串列埠裝置。
-
Mini串列埠,在BCM2837晶片手冊中簡稱UART1。
本實驗使用PL011串列埠裝置。Mini串列埠裝置比較簡單,不支援流量控制(flow control),在高速傳輸過程中還有可能丟包。
BCM2837晶片裡有不少片內外設複用相同的GPIO介面,這稱為GPIO可選功能配置(GPIO Alternative Function)。GPIO14和GPIO15可以複用UART0和UART1串列埠的TXD引腳和RXD引腳,如表16.1所示。關於GPIO可選功能配置的詳細介紹,讀者可以查閱BCM2837晶片手冊的6.2節。在使用PL011串列埠之前,我們需要通過程式設計來使能TXD0和RXD0引腳。
BCM2837晶片提供了 GFPSELn暫存器用來設定GPIO可選功能配置,其中 GPFSEL0用來配置GPIO0~GPIO9,而GPFSEL1用來配置GPIO10~GPIO19,以此類推。其中,每個GPIO使用3位來表示不同的含義。
-
000:表示GPIO設定為輸入
-
001:表示GPIO設定為輸出。
-
100:表示GPIO配置為可選項0。
-
101:表示GPIO配置為可選項1。
-
110:表示GPIO配置為可選項2。
-
111:表示GPIO配置為可選項3。
-
011:表示GPIO配置為可選項4。
-
010:表示GPIO配置為可選項5。
首先在include/asm/base.h標頭檔案中加入樹莓派暫存器的基地址。
#ifndef _P_BASE_H
#define _P_BASE_H
#ifdef CONFIG_BOARD_PI3B
#define PBASE
0x3F000000
#
else
#define PBASE
0xFE000000
#endif
#endif
/*_P_BASE_H */
下面是PL011串列埠的初始化程式碼。
void uart_init (
void )
{
unsigned int selector;
selector = readl(GPFSEL1); selector &= ~(
7<<
12);
/* 為GPIO14設定可選項0*/
selector |=
4<<
12;
selector &= ~(
7<<
15);
/* 為GPIO15設定可選項0 */
selector |=
4<<
15;
writel(selector, GPFSEL1);
上述程式碼把 GPIO14 和 GPIO15 設定為可選項0,也就是用作PL011 串列埠的RXD0 和TXD0引腳。
writel(
0, GPPUD);
delay(
150);
writel((
1<<
14)|(
1<<
15), GPPUDCLK0);
delay(
150);
writel(
0, GPPUDCLK0);
通常GPIO引腳有3個狀態——上拉(pull-up)、下拉(pull-down)以及連線(connect)。連線狀態指的是既不上拉也不下拉,僅僅連線。上述程式碼已把GPIO14和GPIO15設定為連線狀態。
下列程式碼用來初始化PL011串列埠。
/* 暫時關閉串列埠 */
writel(
0, U_CR_REG);
/* 設定波特率 */
writel(
26, U_IBRD_REG);
writel(
3, U_FBRD_REG);
/* 使能FIFO */
writel((
1<<
4) | (
3<<
5), U_LCRH_REG);
/* 遮蔽中斷 */
writel(
0, U_IMSC_REG);
/* 使能串列埠,開啟收發功能 */
writel(
1 | (
1<<
8) | (
1<<
9), U_CR_REG);
接下來實現如下幾個函式以收發字串。
void uart_send(char c)
{
while (readl(U_FR_REG) & (
1<<
5))
;
writel(c, U_DATA_REG);
}
char uart_recv(
void)
{
while (readl(U_FR_REG) & (
1<<
4))
;
return(readl(U_DATA_REG) &
0xFF);
}
uart_send()和uart_recv()函式分別用於在while迴圈中判斷是否有資料需要發生和接收,這裡只需要判斷U_FR_REG暫存器的相應位即可。
接下來編寫Makefile檔案。
board ?= rpi3
ARMGNU ?= aarch64-linux-gnu
ifeq ($(board), rpi3)
COPS += -DCONFIG_BOARD_PI3B
QEMU_FLAGS += -machine raspi3
else ifeq ($(board), rpi4)
COPS += -DCONFIG_BOARD_PI4B
QEMU_FLAGS += -machine raspi4
endif
COPS += -g -Wall -nostdlib -nostdinc -Iinclude
ASMOPS = -g -Iinclude
BUILD_DIR = build
SRC_DIR = src
all : benos.bin
clean :
rm -rf $(BUILD_DIR) *.bin
$(BUILD_DIR)/%_c.o: $(SRC_DIR)/%.c
mkdir -p $(@D)
$(ARMGNU)-gcc $(COPS) -MMD -c $< -o $@
$(BUILD_DIR)/%_s.o: $(SRC_DIR)/%.S
$(ARMGNU)-gcc $(ASMOPS) -MMD -c $< -o $@
C_FILES = $(wildcard $(SRC_DIR)
/*.c)
ASM_FILES = $(wildcard $(SRC_DIR)/*.S)
OBJ_FILES = $(C_FILES:$(SRC_DIR)/%.c=$(BUILD_DIR)/%_c.o)
OBJ_FILES += $(ASM_FILES:$(SRC_DIR)/%.S=$(BUILD_DIR)/%_s.o)
DEP_FILES = $(OBJ_FILES:%.o=%.d)
-include $(DEP_FILES)
benos.bin: $(SRC_DIR)/linker.ld $(OBJ_FILES)
$(ARMGNU)-ld -T $(SRC_DIR)/linker.ld -o $(BUILD_DIR)/benos.elf $(OBJ_FILES)
$(ARMGNU)-objcopy $(BUILD_DIR)/benos.elf -O binary benos.bin
QEMU_FLAGS += -nographic
run:
qemu-system-aarch64 $(QEMU_FLAGS) -kernel benos.bin
debug:
qemu-system-aarch64 $(QEMU_FLAGS) -kernel benos.bin -S -s
board用來選擇板子,目前支援樹莓派3和樹莓派4。
ARMGNU用來指定編譯器,這裡使用aarch64-linux-gnu-gcc。
COPS和ASMOPS用來在編譯C語言和組合語言時指定編譯選項。
-
-g:表示編譯時加入除錯符號表等資訊。
-
-Wall:開啟所有警告資訊。
-
-nostdlib:表示不連線系統的標準啟動檔案和標準庫檔案,只把指定的檔案傳遞給聯結器。這個選項常用於編譯核心、bootloader等程式,它們不需要標準啟動檔案和標準庫檔案。
-
-nostdinc:表示不包含C語言的標準庫的標頭檔案。
上述檔案最終會被編譯連結成名為kernel8.elf的.elf檔案,這個.elf檔案包含了除錯資訊,最後使用objcopy命令把.elf檔案轉換為可執行的二進位制檔案。
在Linux主機上使用make命令編譯檔案。在編譯之前可以選擇需要編譯的板子型別。例如,要編譯在樹莓派3上執行的程式,可使用如下命令。
$
export board=rpi3
$ make
要編譯在樹莓派4上執行的程式,可使用如下命令。
$
export board=rpi4
$ make
在放到樹莓派之前,可以使用QEMU虛擬機器來模擬樹莓派以執行我們的裸機程式,可直接輸入“make run”命令。
$ make run
qemu-system-aarch64 -machine raspi3 -nographic -kernel benos.bin
Welcome BenOS!
也可輸入如下命令。
$ qemu-system-aarch64 -machine raspi3 -nographic -kernel benos.bin
如果讀者想用QEMU虛擬機器模擬樹莓派4B,那麼需要打上樹莓派4B的補丁並且重新編譯QEMU虛擬機器。
要在樹莓派上執行剛才編譯的裸機程式,需要準備一張格式化好的MicroSD卡。
-
使用MBR分割槽表。
-
格式化boot分割槽為FAT32檔案系統。
參照實驗3-1中介紹的方法燒錄MicroSD卡,這樣就可以得到格式化好的boot分割槽和燒錄的樹莓派韌體。讀者也可以使用Linux主機上的分割槽工具(比如GParted)來格式化MicroSD卡,把樹莓派韌體複製到這個FAT32分割槽裡,其中包括如下幾個檔案。
-
bootcode.bin:載入程式。樹莓派復位上電時,CPU處於復位狀態,由GPU負責啟動系統。GPU首先會啟動固化在晶片內部的韌體(BootROM程式碼),讀取MicroSD卡中的bootcode.bin檔案,並裝載和執行bootcode.bin中的載入程式。樹莓派4B已經把bootcode.bin載入程式固化到BootROM裡。
-
start4.elf:樹莓派4上的GPU韌體。bootcode.bin載入程式檢索MicroSD卡中的GPU韌體,載入韌體並啟動GPU。
-
start.elf:樹莓派3上的GPU韌體。
-
config.txt:配置檔案。GPU啟動後讀取config.txt配置檔案,讀取Linux核心映像(比如kernel8.img等)以及核心執行引數等,然後把核心映像載入到共享記憶體中並啟動CPU,CPU結束復位狀態後開始執行Linux核心。
把benos.bin檔案複製到MicroSD卡的boot分割槽,修改裡面的config.txt檔案。
<config.txt檔案>
[pi4]
kernel=benos.bin
max_framebuffers=
2
[pi3]
kernel=benos.bin
[all]
arm_64bit=
1
enable_uart=
1
kernel_old=
1
disable_commandline_tags=
1
插入MicroSD卡到樹莓派,連線USB電源線,使用Windows端的串列埠軟體可以看到輸出,如圖16.6所示。
圖16.6 輸出歡迎語句
下載入門篇第二版全套海量資料
登入“奔跑吧linux社群”微信公眾號,在微信公眾號裡輸入“奔跑吧2”,下載奔跑吧第二版入門篇全套資料:
-
全套實驗環境,基於Ubuntu 20.04製作的VMware/VirtualBox虛擬機器映象
-
實驗指導手冊
-
電子課件
-
實驗參考程式碼
-
插圖下載
-
勘誤
-
免費視訊
-
Docker映象
好多同學問金色那本實驗指導手冊哪裡可以買?
其實,實驗指導手冊pdf版本已經開源,大家可以免費下載,自由列印。
列印小貼士:到淘寶上隨便找一家列印店,列印B5大小即可。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70005277/viewspace-2872517/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 綜合能力訓練:在樹莓派上動手寫一個小OS(6):實驗16-5:程式建立實驗樹莓派
- 綜合能力訓練:在樹莓派上動手寫一個小OS(4):實驗16-3:實現printk函式樹莓派函式
- 樹莓派上利用Tensorflow實現小車的自動駕駛樹莓派自動駕駛
- 在樹莓派上安裝 Ubuntu MATE樹莓派Ubuntu
- 在樹莓派上執行 DOS 系統樹莓派
- 在樹莓派上安裝mysql/MariaDB樹莓派MySql
- 在樹莓派上搭建智慧家居閘道器樹莓派
- OpenYurt 入門 - 在樹莓派上玩轉 OpenYurt樹莓派
- 樹莓派上安裝php樹莓派PHP
- 在樹莓派上編譯安裝golang環境樹莓派編譯Golang
- 樹莓派上配置伺服器樹莓派伺服器
- 關於在windows,ubuntu,樹莓派上安裝使用opencvWindowsUbuntu樹莓派OpenCV
- 在樹莓派上安裝go環境很簡單樹莓派Go
- 樹莓派使用入門:在樹莓派上使用 Mathematica 進行高階數學運算樹莓派
- 在樹莓派上部署yolo模型推理並使用onnx加速樹莓派YOLO模型
- 在樹莓派上設定家庭網路的家長控制功能樹莓派
- 在樹莓派上安裝c++版本的opencv並執行樹莓派C++OpenCV
- 如何在樹莓派上部署Kubernetes樹莓派
- 樹莓派上使用Slowloris進行DDoS攻擊樹莓派
- 樹莓派上使用螞蟻礦機挖礦樹莓派
- 樹莓派 - 實戰篇 [基於 websocket 實現手機遠端控制樹莓派小車]樹莓派Web
- 在 PC 上嘗試樹莓派的 PIXEL OS樹莓派
- 樹莓派使用入門:如何啟動一個新的樹莓派樹莓派
- 使用 Ansible 在樹莓派上構建一個基於 Linux 的高效能運算系統樹莓派Linux
- 樹莓派上安裝USB網路攝像頭樹莓派
- 如何在樹莓派上執行雷神之錘III樹莓派
- 在struts2框架中實現手動處理輸入驗證框架
- [譯] 如何輕鬆地在樹莓派上使用深度學習檢測物件樹莓派深度學習物件
- 亞馬遜詳解如何使用MXNet在樹莓派上搭建實時目標識別系統亞馬遜樹莓派
- web 練手 JavaScript 輸出WebJavaScript
- 結構體綜合訓練結構體
- 物件導向綜合訓練物件
- 超強教程!在樹莓派上構建多節點K8S叢集!樹莓派K8S
- 在樹莓派上開發SpringBoot 之使用VSCode遠端開發樹莓派Spring BootVSCode
- 計算機實驗室之樹莓派:課程 11 輸入02計算機樹莓派
- 教你如何在樹莓派上搭建語音識別服務樹莓派
- 樹莓派上安裝2.8寸TFT觸控式螢幕樹莓派
- 樹莓派使用入門:如何購買一個樹莓派樹莓派