引言
本文主要以RISC-V開發板上安卓的實現過程為切入點,討論了在安卓上添加新的指令架構(ISA)和板級平臺支持的各個階段,概述了每個階段針對架構需要添加哪些支持,涉及開發過程中一些常見的問題和注意點;可以作為安卓指令架構支持和板級開發的參考。本文內容主要作為概述,其中細節較多的部分將會在其他文章展開討論。
為什麼要做安卓的RISC-V支持
處理器指令架構在數十年前就開始百花齊放,GCC支持的指令架構數達50+個之多,[email protected]註冊的EM架構數達252個之多。這些指令架構通常由各家芯片廠商或研究機構獨立發起,彼此間相對獨立,都需要投入大量人力進行軟硬件支撐。出於“Instruction Sets Want to be Free”這一願景,RISC-V作為一個開源、通用、穩定、簡潔、獨立的指令架構被提出,目標是讓各種規模、微結構、軟件棧的計算系統都能基於一種統一的指令架構有效工作。自 2011年推出以來RISC-V迅速地普及,其軟件生態也逐步完善;包括GCC/Bintuils工具鏈、Glibc庫、Linux內核等一系列基礎設施得到了支持並upstream至開源主枝,Fedora、Debian、openSUSE、Gentoo等一眾發行版官方支持了RISC-V架構,Go、OpenJDK、Free Pascal、Rust、Node.js等等高級語言編譯/運行環境都支持了RISC-V的後端支持。然而RISC-V在Android生態軟件支持上仍有較大的空缺,基於RISC-V架構支持了Android為後續移動/智能終端產品基於RISC-V架構的處理器落地提供了一種較低成本的可能性;現有上層應用不需要較大的改動就可以平滑的移動到使用自由ISA、自由SoC、自由軟件的RISC-V平臺上。
RISC-V ISA的安卓軟件棧支持
安卓軟件棧主要包括系統內核、硬件抽象、運行時、框架層、應用五個層次的近千個軟件包。其中作為基礎的Linux內核、GCC工具鏈、Clang/LLVM工具鏈已經支持了RISC-V架構;因此對的主要支持工作集中於RISC-V ISA的編譯框架支持、Bionic C庫的ISA支持、ART JAVA運行時的ISA支持、RVB-ICE開發闆闆級的驅動對接、OPENGL的對接、Chromium webview瀏覽器幾個部分;其他依賴包括NDK、VNDK、emulator、unwind解析庫、編解碼庫等等。本文後續將從開發順序為敘事線索,對上述指令架構需要支持的模塊進行介紹。
如何支持
以下將分五個階段對安卓的RISC-V ISA支持和RVB-ICE開發板上的板級支持進行概要說明:
Step 1. 準備預編譯工程
安卓源代碼樹包含一個prebuilts目錄,裡面存放了包括host工具、開發套件、模擬器、二進制的abi描述文件幾類預編譯工程。使用預編譯工程在減少總體編譯量、提升二進制兼容性、使模塊劃分更清晰的同時也帶來了更多的架構支持工作量:不盡相同的編譯框架、完全獨立的編譯腳本、單獨的ISA的編譯支持。以下將對各個預編譯工程逐個進行介紹:
工具鏈
安卓的最新源碼已經完全使用Clang來做系統的整體構建,但仍使用libgcc相關的函數庫,也會使用-fuse-ld=bfd,-no-integrated-as之類的選項來調用GCC的彙編器,連接器。因此添加RISC-V ISA支持首先需要支持Clang/LLVM和GCC兩套工具鏈。Clang/LLVM工程可以通過以下命令拉取:
repo init -u https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/platform/manifest -b llvm-toolchain repo sync
該工程中需要為RISC-V添加工具前綴、架構配置、運行時庫相關的支持;並通過build.py腳本進行構建:
cd toolchain/llvm_android ./build.py
按照以下流程完成工具的生成:
玄鐵910特有的擴展指令優化則可以通過在toolchain/llvm-project/llvm/lib/Target/RISCV下額外添加指令和寄存器tablegen相關描述,使安卓整體工程能得到玄鐵910擴展指令的加速。 GCC工具鏈則可以通過RISC-V官方開源的構建工程生成:
git clone https://github.com/riscv/riscv-gnu-toolchain.git git submodule update --init --recursive 修改target triplets ./configure --prefix=/opt/riscv make linux
NDK/VNDK
在擁有兩套預編譯工具鏈之後即可著手NDK, VNDK兩套開發套件的生成。NDK是一套包含了眾多平臺庫用於C/C++程序開發工具套件,包含C/C++源碼、mk描述的原生工程;安卓內部的許多模塊都依賴NDK,如system、frameworks路徑下各類的原生程序;可以通過ndk-build或gradle編譯出適配各個平臺兼容API版本的原生執行程序:
foo@bar:[hello]$ ~/android-ndk/android-ndk-r20/ndk-build Android NDK: APP_PLATFORM not set. Defaulting to minimum supported version android-16. [arm64-v8a] Compile : hello-android <= hello.c [arm64-v8a] Executable : hello-android ... [riscv64] Compile : hello-android <= hello.c [riscv64] Executable : hello-android [riscv64] Install : hello-android => libs/riscv64/hello-android ...
NDK構建工程可以通過以下命令拉取:
repo init -u https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/platform/manifest -b ndk-r20 repo sync
其中rxx為版本號需要與系統版本要求的最小API相適應。該工程中需要為RISC-V添加工具主要需要添加版本、位寬、路徑相關的配置支持;並通過checkbuild.py腳本進行構建:
./ndk/checkbuild.py
VNDK則是用於讓供應商實現其 HAL 對接的一套原生庫的集合,包括框架共享庫和SP-HAL兩個部分。可以讓系統在升級的時候其供應商分區保持不變,避免API/ABI變化所引發的問題;VNDK可以基於安卓的主工程使用以下命令生成:
make vndk dist
Linux內核
Linux內核在安卓中通常也通過預編譯方式存放,通用模擬器使用的內核鏡像存放於prebuilts/qemu-kernel而設備板級通常存放於device目錄;一般來說開發板會使用Image二進制或gz文件,模擬器可以使用elf文件以方便調試 :
foo@bar:~$ cd aosp foo@bar:[aosp]$ file prebuilts/qemu-kernel/riscv64/ranchu/kernel-qemu prebuilts/qemu-kernel/riscv64/ranchu/kernel-qemu: ELF 64-bit LSB executable, UCB RISC-V, version 1 (SYSV), statically linked, BuildID[sha1]=60388b123f1c60053407fd899f9acceed43ac267, with debug_info, not stripped foo@bar:[aosp]$ file device/linaro/hikey-kernel/Image.gz-dtb-hikey960-4.19 device/linaro/hikey-kernel/Image.gz-dtb-hikey960-4.19: gzip compressed data, max compression, from Unix, original size 7301488
Linux內核工程可以通過以下命令拉取:
repo init -u https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/kernel/manifest -b common-android-5.4-stable repo sync
RISC-V的Linux內核在4.15版本就被upstream至kernel.org主枝,因此只需要添加相關配置文件就可以使用腳本構建安卓的內核鏡像:
New file: common/arch/riscv/configs/gki_defconfig New file: common/build.config.gki.riscv64 New file: common/build.config.riscv64 構建內核: BUILD_CONFIG=common/build.config.gki.riscv64 build/build.sh
模擬器相關的驅動通常以goldfish開頭,在內核中已經有實現;其中goldfish_pipe則較為泛用,會被用於主機通信、adb、網絡、opengl渲染等等功能,其他驅動都對應一個專門的外設功能。模擬器中的虛擬平臺的設備樹要配置與內核compatible一致的,否則會導致設備缺失、協議不兼容等相關問題。
foo@bar: [common]$ find drivers -name goldfish*.c drivers/input/keyboard/goldfish_events.c #虛擬觸摸輸入 drivers/platform/goldfish/goldfish_pipe_base.c #主機通信接口 drivers/platform/goldfish/goldfish_pipe_v1.c #主機通信接口 drivers/platform/goldfish/goldfish_pipe_v2.c #主機通信接口 drivers/power/supply/goldfish_battery.c #虛擬電量設備 drivers/staging/goldfish/goldfish_audio.c #虛擬音頻輸出 drivers/staging/goldfish/goldfish_sync.c drivers/tty/goldfish.c #虛擬終端輸出 drivers/video/fbdev/goldfishfb.c #虛擬FrameBuffer顯示
RVB-ICE開發闆闆級則需要將所有未upstream至內核官方源碼樹的第三方設備驅動與內核代碼整合或使用ko方式進行模塊編譯。這些驅動通常包括存儲、顯示、觸控、傳感器、USB、藍牙、攝像、定位、音頻、硬件codec等等,需要在後續服務調試過程中與HAL進行對接,向上層應用提供基礎服務。此外開發過程中也會發現一些模塊缺失或者兼容性問題,需要在後續調試過程中不斷的進行調整和適配。
模擬器
安卓的模擬器基於QEMU實現,通過一箇中間glue層進行對接,最外層實現了emulator封裝;提供了虛擬設備管理、鏡像指定、snapst緩存、gpu加速、攝像頭模擬、網絡映射等功能。還可以針對需要編譯成TV、電話、穿戴、平板、車載等不同配置。以通過以下命令拉取:
repo init -u https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/platform/manifest -b emu-master-dev repo sync
emulator提供的大部分特性都與架構無關,而external/qemu/target/riscv目錄下已經支持了RISC-V相關的tcg指令反應和C的help函數,因此RISC-V相關支持只需要添加cmake編譯支持、emulator的架構字串、和external/qemu/hw/riscv/下的goldfish虛擬設備即可。虛擬設備文件主要包括內存配置、設備樹創建、中斷控制器初始化、虛擬設備創建、固件加載幾個部分;其中需要注意設備數和設備創建和內核配置的一致性,不然很容易出現設備匹配問題:
... static const struct MemmapEntry { //內存分區 ... static void riscv_ranchu_board_init(MachineState *machine) { ... /* 初始化內存分區 */ memory_region_init_ram(main_mem, NULL, "riscv_ranchu_board.ram", machine->ram_size, &error_fatal); ... /* 創建設備樹 */ fdt = create_fdt(s, memmap, machine->ram_size, machine->kernel_cmdline); ... /* 創建goldfish設備 */ create_device(s, fdt, RANCHU_GOLDFISH_FB); ... /* 加載opensbi */ riscv_load_firmware (machine->firmware, memmap[RANCHU_DRAM].base, NULL); ...
之後使用腳本構建模擬器包:
cd external/qemu/ ./android/rebuild.sh
其他
其他預編譯工程還包括clang-tool、gdbserver等等。clang-tool包含ABI比較、版本管理相關工具,由prebuilts/clang-tools/build-prebuilts.sh腳本生成。而gdbserver則由binutils-gdb工程靜態預編譯生成,可以用於原生C/C++程序和ART底層執行環境的調試。
Step 2. 編譯框架和構建支持
安卓 7.0版本之後代碼構建通過Blueprint和Soong實現。bp文件作用取代過去的mk文件包含編譯目標模塊的名稱、代碼、鏈接庫、編譯鏈接選項等參數。而Soong(會調用Blueprint相關工具)則類似於make命令的展開部分負責將bp轉換為ninja文件,ninja文件主要保存於out/soong/build.ninja,通過調用ninja -f build.ninja命令就能根據規則完成各個模塊的編譯。
除了編譯框架之外,RISC-V架構支持還包括原生C/C++程序、Java運行時兩個部分的支持。原生C/C++程序支持主要包括Bionic、opengl、protobuf、libunwindstack等模塊,還包括一些可以進行彙編優化的加解密、音視頻、AI模塊;JAVA運行時則集中於ART目錄實現。
編譯框架支持
安卓的編譯框架支持主要位於build目錄下,又分為make和soong兩部分。soong部分針對架構主要要維護函數庫路徑、編譯鏈接選項、ABI、架構名稱字串等相關內容。make部分則主要包括架構名稱匹配、工具鏈的路徑和參數和通用板級相關的配置。預編譯工程中的VNDK、SDK都對編譯框架有依賴,因此在生成這兩個預編譯工程前要求編譯框架和bionic都得到了相關支持。為RISC-V添加虛擬機平臺主要需要添加以下文件:
1. core/combo/TARGET_linux-riscv64.mk //編譯ABI,toc生成規矩,鏈接目標配置 2. core/combo/arch/riscv64/riscv64.mk //用於指定平臺相關的額外編譯選項,如指令集 3. target/board/generic_riscv64/BoardConfig.mk //模擬器平臺定義,引用一些通用外設或鏡像配置 4. target/board/generic_riscv64/device.mk //定義JAVA虛擬機運行參數等一些設備配置 5. target/board/generic_riscv64/system.prop //定義系統運行環境的prop變量 6. target/product/aosp_riscv64.mk //定義產品名稱,依賴包列表等配置 7.
RVB-ICE開發板的板級平臺支持與通用模擬器配置略有不同,存放於device下的對應板級目錄內,主要包括:
Android.bp/Android.mk //設備頂層編譯腳本 AndroidProducts.mk //產品選單配置 BoardConfigCommon.mk //板級配置,包含架構、功能開關、recovery等相關配置 common.kl //按鍵觸摸映射 device-common.mk //定義依賴包列表等配置 [device_name].mk //定義產品名稱和相關字串 [device_name]/BoardConfig.mk //板級配置,包括名稱、啟動參數、鏡像大小等等 [device_name]/fstab.[device_name] //產品的掛載定義 [device_name]/device-[device_name].mk //定義設備文件安裝規則 init.*.rc //設備特有的初始化腳本 manifest.xml //支持的hal接口定義 system.prop //定義系統運行環境的prop變量
其他部分主要為一些特性支持和HAL對接會隨設備的複雜度增加而增加:
audio //音頻設備hal對接 bluetooth //藍牙設備hal對接 gralloc //圖形內存管理對接 gpu //gpu加速的hal對接 power //功耗管理的hal對接 overlay //源代碼配置覆蓋 recovery //恢復分區支持 sensorhal //傳感器hal對接 sepolicy //設備的安全規則 vndk //vndk的構建工程 ...
Bionic支持
Bionic是Android的C函數庫;區別於glibc會提供ISO,POSIX,UNIX,XOPEN,XPG多套接口標準支持,被用與不同的類UNIX系統進行對接;bionic只需要實現與linux內核相關對接,主要覆蓋ISO和POSIX相關接口。除了標準C庫、線程庫以外,還提供了浮點數學庫、動態鏈接器和部分Android特有的接口(如systrace、scudo、property等)。 對於RISC-V ISA支持來說,Bionic中主要實現以下幾部分內容:
libc部分主要對常用內存操作函數、內核提供的系統調用、數據結構等內容進行了封裝向用戶態程序提供了標準化的接口;Bionic的線程庫也位於libc中實現,大部分線程接口如創建、等待、取消都為公共實現;架構相關的主要為tls部分,需要為RISC-V定義相關layout,各個數據結構(tp、tcb、dtv)的獲取等等。 動態鏈接程序使用庫函數依賴於鏈接器相關的支持,鏈接器負責將dso加載至任意內存地址,並對需要重定位的指令進行修改使其能正常的執行跳轉集地址獲取指令;Bionic中的鏈接器公共代碼實現了8種常用重定位的定義,一般架構只需要將對應重定位序號進行對接即可。
其他原生程序支持
其他原生程序支持還包括程序堆棧回溯支持、OPENGL支持、彙編加速優化、protobuf支持等等。 安卓中為程序錯誤提供完善的dump和調用棧回溯功能,其中native層的堆棧回溯主要基於的libunwindstack實現。RISC-V相關支持主要需要添加ptrace調用的寄存器上下文的格式,棧幀的寄存器排布和elf信息解析相關功能。 OPENGL的支持則包括GL接口entry樁點,GPU設備對接兩個部分。GL接口entry樁點,包含一段架構特有的彙編入口實現,負責加載opengl tls數據結構和準備接口參數。GPU設備對接通常需要和IP供應商對接,使用對應sysroot的工具鏈生成依賴的圖形庫文件以保證加載時的依賴關係正確性;此外還需要對gralloc(內存管理),compositor(合成單元),drm(圖形渲染框架)進行對接,使上層程序可以正常的調用GPU底層接口。 對於安卓這種業務覆蓋密集運算、音視頻、加解密、3d渲染、AI的系統來說,如何使用適當的軟件編碼來儘可能的發揮硬件性能尤為重要。如bionic的內存/字符串操作/浮點運算、boringssl的大數模乘、編解碼的乘累加,NN引擎的數組向量化運算都可以針對指令架構進行優化;使用大位寬、可非對齊訪問、複合功能、向量化的擴展指令進行優化往往能給熱點程序提供數倍到數十倍的性能提升。
ART支持
Android應用程序是基於JAVA語言編譯生成的dalvik字節碼程序。由於linux系統無法直接執行dalvik字節碼的應用程序,Android系統上集成了一個可以運行dalvik字節碼程序的虛擬機。虛擬機的作用在於將dalvik字節碼的功能通過系統提供的庫、CPU的指令,完成對應字節碼的功能。 從Android-5開始,DVM被ART取代(但是很多執行文件的名字還是叫做delvik),ART引入了AOT技術,在應用安裝或者手機充電的時候,ART會利用dex2oat工具編譯應用代碼,將其編譯為與目標機器CPU想匹配的代碼。其過程可以用下圖描述。
從上圖ART虛擬機的運行流程中,主要有三部分內容是移植RISC-V的關鍵:
- AOT編譯器:圖中.oat文件的生成工具,oat文件包含可執行二進制碼的文件。AOT編譯器作用是將dex字節碼編譯成oat文件,缺省配置下,在編譯時或者安裝時,會調用dex2oat來完成,
- JIT編譯器:圖中紫色部分,dex字節碼運行過程中,ART記錄執行的方法是否是熱點方法,並生成profiling信息。JIT編譯器的作用根據profiling信息對熱點方法進行編譯。
- Interpreter:dex字節碼解釋器,用於執行Android的dex字節碼
此外,無論Interpreter還是編譯器都會用到彙編器以及反彙編器。 接下來的內容,我們就從彙編器,解釋器,編譯器幾個方面對移植工作做個簡單的介紹
彙編器
彙編器的功能是將編譯的指令轉換成機器碼,是ART中編譯器部分的基礎部件。在移植到RISC-V體系架構過程中,完成RISC-V所有指令集的彙編功能。
解釋器
解釋器的作用就是解釋執行dex字節碼。RISC-V指令架構支持過程中,該部分工作集中在:
- dex字節碼翻譯成RISC-V指令
- c++/java現場轉換的context的保存和恢復
編譯器
AOT編譯器和JIT編譯器在ART中使用的是同一套編譯框架,複用同一套實現代碼。兩個編譯器的目的都是為了將dex字節碼編譯成可執行二進制碼。
從上圖可以看到,編譯器經過優化後,得到ART HInstruction,再由體系結構相關的後端處理,生成對應體系結構的指令。上圖中標註為黃色部分為RISC-V體系結構主要完成工作:
- 優化遍:指令簡化(Instruction Simplifier), Intrinsic,Vector
- RISC-V後端:指令生成(Code Gen),寄存器分配
Step 3. 原生小系統啟動
在mksh命令行和toybox小工具集能夠正常基於安卓生成之後即可開始進行原生程序相關的調試。本階段需要在完成系統分區和鏡像燒寫,boot的引導, 內核的啟動,文件系統的加載,運行各類初始化rc腳本,啟用selinux相關環境,啟動rc腳本註冊的各種服務,初始化命令行,最後進入循環等待各類事務的處理。
內核啟動
在系統啟動調試初始階段通常會使用gdb加載linux內核(打包自制的ramdisk)方式調試基礎的c程序運行。內核在kernel_init中調用prepare_namespace完成ramdisk掛載後就會通過ramdisk_execute_command運行init進程,init進程一般使用靜態編譯,因此可以直接使用gdb調試從load_elf_binary接口向下通過異常處理函數進入用戶態,觀察程序出錯點,此時出現的問題通常只涉及crtbegin入口的參數和跳轉處理,系統調用的傳參和返回,以及init-array是否被正常調用。當走通靜態程序執行後即可在init中調用動態編譯的程序,主要調試動態庫加載,符號重定位等相關內容。當以上過程都順利走通,就表示原生程序運行已經基本走通,可以開始進一步正規的init啟動過程調試了。
系統初始化
安卓的初始化函數位於system/core/init下,分為first_stage_init.cpp和init.cpp兩個階段。第一階段主要負責節點的創建和部分文件系統的掛載(要求boot,system,vender,data等分區在開發板上已經按照分區表進行行了燒寫,RVB-ICE開發板使用的是GPT),log系統的初始化,一些基礎環境變量的設置;之後會調用selinux_setup加載預編譯的規則和上下文文件,為各個文件節點配置安全屬性,在調試時selinux進行修改通常會帶來不少額外的工作量,因此在開發過程中通常在系統的bootargs中添加permissive選項關閉相應校驗功能;當selinux完成加載之後會調用第二階段的初始化,本階段主要包括property的設置、二階段的安全上下文配置、ActionManager的初始化、Keychord的隊列維護、命令行的啟動和原生服務的啟動。至此係統的初始化已經完成大半,原生部分也僅剩餘服務部分需要進行調試了。
在第二階段的init loop循環中會維護包含SurfaceFlinger、netd、vold、apex、ashmem、installd、media等模塊的調試。這些服務通過/root或/system/etc/init下的rc文件進行維護,在特定扳機被觸發或property被設置後啟動。這些服務模塊大多依賴板級平臺上的HAL對接,device下需要實現包括存儲、wifi、gpu、音頻的接口支持,並通過相應用例以保證服務執行運行。安卓啟動動畫也會在本階段SurfaceFlinger和bootanimation走通後正常顯示。本階段主要使用adb+gdbserver配合logcat吐出的日誌信息進行調試。
Step 4. Zygote與Java服務
zygote啟動
zygote進程是所有java服務的父進程,它是由init進程從配置⽂件中獲取app_process程序的啟動⽅式(包括參數)並啟動的;會進⼊⽂件 frameworks/base/core/jni/AndroidRuntime.cpp,然後進⼊此⽂件中的函數:startVm正式啟動ART虛擬機,然後調用通過native方法"com.android.internal.os.ZygoteInit",啟動Zygote進程,進入Zygote的JAVA環境。
JAVA服務啟動
Java服務啟動位於frameworks/base/services/java/com/android/server/SystemServer.java,分BootstrapServices、CoreServices、OtherServices三個階段啟動各類Java服務,服務使用高級語言編寫與指令架構基本無關;但在系統層面會關心底層與HAL對接的硬件模塊實現是否完全,對應系統服務deamon是否在正常的提供服務,系統運行的速度是太慢觸發了TIMEOUT機制,JAVA虛擬機執行出錯是否為ART實現問題等等。在開發過程中常常有許多模塊由於架構支持不完全等原因被暫時繞過,後續服務啟動就會發現執行出錯,此時通常打開對應服務的log後通過logcat就能看到對應錯誤原因,再針對性的調試對應模塊依賴就能解決大多數的服務問題。在模擬器調試中由於qemu執行指令較慢會出現大量的服務啟動超時問題,大部分的超時問題可以通過log打印查看,再改長對應服務延時即可解決。上層java應用邏輯也可以使用JDB,單步java代碼查看程序路徑進行調試。
Step 5. launcher桌面顯示
在原生程序和Java服務都調試穩定的理想狀況下,系統自然可以啟動到桌面。然而實際的系統調試往往並沒有那麼順利,常見問題現象通常有啟動動畫循環播放、模塊缺失、系統服務執行奔潰等等。這種情況下通常會其他架構平臺的運行狀況進行對比,依次確認SurfaceFlinger、WindowsManager相關服務是否正常的初始化;Wallpaper、systemUI、Launcher三個app的程序邏輯是否符合預期;配合原生程序和java服務調試過程中相關的手段,就能解決大部分問題了。此外使用系統單元測試用例保證每個模塊工作邏輯正常也會對整體系統調試提供很大的幫助。