- libc/SECCOMP_*.TXT //seccomp黑白名单
- libc/SYSCALLS.TXT //系统调用列表
- libc/arch-common/bionic/crtbegin.c //c函数跳转入库
- libc/arch-riscv64/bionic/* //汇编实现的通用系统接口
- libc/arch-riscv64/generic/* //mem/str类函数汇编优化
- libc/arch-riscv64/syscalls/* //系统调用接口
- libc/include/bits/elf_riscv64.h //重定位类型声明
- libc/include/bits/fenv_riscv64.h //浮点异常定义
- libc/include/sys/ucontext.h //上下文保存结构定义
- libc/kernel/uapi/asm-riscv/* //内核头文件
- libc/libc.map.txt //符号输出控制文件
- libc/private/bionic_asm_riscv64.h //架构相关对齐,layout定义
- libc/private/*tls* //tls相关处理
- libc/seccomp/* //安全接口对接声明
- libm/riscv64/fenv.c //浮点控制接口实现
- linker/* //重定位相关实现
- tests/prebuilt-elf-files/riscv64/* //动态加载测试用例
- tools/versioner/* //版本工具支持
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服务调试过程中相关的手段,就能解决大部分问题了。此外使用系统单元测试用例保证每个模块工作逻辑正常也会对整体系统调试提供很大的帮助。
本文转自平头哥芯片开放社区(occ),更多详情请点击https://occ.t-head.cn/development/software?channelName=1 。