Android工程搭建与系统启动
Android工程集成一直是工作中的部分内容,这里简要记述一下Android集成中涉及Kernel、Android修改和配置的要点,在一个全新的板子上配置时需要注意一些什么内容等等。
Android源码下载
Platform Source Code
Android源码版本号确认:
- 编译的时候在终端中一开始就会打印出来:PLATFORM_VERSION:2.3.1
- 直接去make文件中去看:
build/core/version_defaults.mk
,搜索该文件中的PLATFORM_VERSION值,然后与上面Android Version描述中对比
Kernel Source Code
① 制作独立ramdisk.img(不启动Android的)
- 使用busybox制作ramdisk镜像:
TODO...
- 人工手动制作ramdisk进行:
我们用busybox制作了一个miniramdisk可以独立使用,放到了coding代码托管:下载路径
TODO...
② 打/解包ramdisk.img
- 解包ramdisk: 得到ramdisk目录,里面包含root/下的所有文件
mv ramdisk.img ramdisk.img.gz
gunzip ramdisk.img.gz
mkdir ramdisk
cd ramdisk
cpio -i -F ../ramdisk.img
- 打包ramdisk: 将上面解包的ramdisk重新打包生成ramdisk.img
cd ramdisk
find . | cpio -o -H newc | gzip > ../ramdisk-new.img
Android工程配置
Platform Config
① fstab分区表修改:
挂载/data和/cache分区需要通过device/${board}/fstab进行配置。
② system.prop属性配置:
- ro.hardware:
通过kernel的bootargs中参数androidboot.hardware=XXXX带入,在init.cpp中会对这个做解析,然后设置到ro.hardware这个属性中。
- ro.product.hardware:
通过在build/make/target/board/generic_arm64/system.prop中添加ro.product.hardware=xxxx来定义,将来这个文件的内容会被copy合并到system.img中的property文件中。
③ rc文件配置:
④ selinux配置:
Kernel Config
① 串口
② defconfig
③ dts - 需要配置soc上各种东西core、memory、gic、uart、等等
特别需要跟android启动有关的:
* android firmware中配置system、vendor分区
* fb配置,包含它的reserved memory等
* 这里如果没有屏幕只想通过log验证的话,kernel里面有一个vfb设备可以使用
* 配置CONFIG_FB_VIRTUAL=y,同时bootargs中带字段video=vfb指定GPU去找vfb这个framebuffer
以arm64的版本为例:
① 确认编译的版本是arm64的版本
② 将aarch64-linux-android-4.9工具链路径export到shell环境变量中
export PATH=/home/acean/.buildman-toolchains/aarch64-linux-android-4.9/bin/:$PATH
Android工程编译
Platform Compile
source build/envsetup.sh
lunch 2 //以generic_arm64为例
make -jN
以上编译完毕之后,会生成如下几个镜像文件:
- ramdisk.img
- system.img
- userdata.img
- vendor.img
- cache.img
Kernel Compile
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-android- hikey_defconfig //以hikey板子为例
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-android-
以上编译完毕之后,会生成如下镜像文件:
- Image
- dtb
被Uboot加载的镜像的格式化
这里说“格式化”不知道是否合适,其实目的只是通过uboot提供的工具mkimage来指定镜像加载与运行地址,以便uboot运行后能自动加载或者通过bootm等命令手动加载这些image到内存中,然后跳转并运行。
格式化ramdisk
mkimage -A arm64 -O linux -T ramdisk -C none -a 88080000 -n ramdisk -d ramdisk.img ramdisk_a
格式化Image
mkimage -A arm64 -O linux -T kernel -C none -a 80080000 -e 80080000 -n linux-4.10 -d out/tmparm64/arch/arm64/boot/Image hm_uImage
镜像打包成image文件
这里主要指的是kernel镜像、ramdisk镜像、dtb镜像三者或者二者可以打包在一起成boot.img加载,加载到开发板后再进行解包。
Tips:
或者也可以像上面uboot操作临时单独去分别下载
将uImage与ramdisk一起打包
TODO...
将uImage与ramdisk与dtb一起打包
TODO...
下载镜像到开发板
磁盘分区
从磁盘启动android的话,磁盘需要要存在如下分区:
- uboot (可选,有的开发板可能固化在rom中)
- boot (必选)
- ramdisk (可选,有的开发板可能与kernel打包的一起)
- dt (可选,有的开发板可能与boot打包在一起)
- system (必选)
- vendor (必选)
- usrdata (必选)
- cache (必选)
① GPT分区表: ==> 磁盘分区挂载时走/kernel/block/partitions/efi.c
驱动来添加分区
首先,使用diskginus对硬盘进行分区,将以上分区都划分出来,格式化成ext4格式的
其次,修改GPT分区表中PARTNAME(name),使用工具parted,命令格式:
sudo bash //切换到root用户
parted -l //查看所有分区表
parted /dev/sdb //查看image所在的硬盘上分区细节
$$name 6 sda6 //将system区分修改成对应磁盘名字,这里名字对应dts中dev=/dev/block/system,这两边需要一致的,否则从init主动触发的uevent上报会因在sysfs中找不到分区名字而失败
Tips:
注意一般我们会做从/dev/block/system --到-->/dev/block/sda6这种映射
因此通常是是需要将GPT分区表中名字修改成从其它修改成system即可、而并不需要将system再修改成sda6
我这里这样修改,原因是我还没有找到system->sda6映射是在哪里做的,如果不这样修改,会在__mount时找不到对应dev设备
② MBR分区表: ==> 磁盘分区挂载时走/kernel/block/partitions/msdos.c
驱动来添加分区
TODO...
一般使用SOC厂家提供的特定下载工具(比如展讯的ResearchDownload)按照给定的xml分区表进行分区固化。
① GPT分区表:
TODO...
② MBR分区表:
一般嵌入式板子上面,跑Linux的话通常不使用MBR分区,直接用GPT分区格式。
通过Uboot临时下载Ramdisk镜像
这里并不是必须的,一般我们验证的时候,为了调试方便,可以从uboot下载ramdisk.img和uImage文件。
但是系统固定之后,我们会将ramdisk、uImage等全部放到磁盘上,系统启动时uboot会从磁盘去load这些镜像文件的。
一般我们比较常用的是走网络下载方式,通过host侧建立tftpd server,在uboot侧建立tftp guest,两者通过接入同一个路由器来在同一网段IP地址下通信。
① 电脑PC侧(host)配置tftp server:
- 在ubuntu上安装tftp
apt-get install tftp-hpa tftpd-hpa xinetd
- 配置ftfp server
vim /etc/default/tftpd-hpa
TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/tftpboot" ##指定tftp服务目录,将来image放入其中
TFTP_ADDRESS="0.0.0.0:69" ##指定IP和端口
TFTP_OPTIONS="-l -c -s" ##这里是选项,-c是可以上传文件的参数,-s是指定tftpd-hpa服务目录,上面已经指定
- 手动启动tftpd并验证
sudo /etc/init.d/xinetd restart
sudo /etc/init.d/tftpd-hpa restart
ps aux | grep -E 'netd|tftpd' ##查看一下是否已经有了这两个daemon进程
- 本地验证上传下载
cd /tftpboot ##进入服务目录
tftp localhost ##链接本机
tftp>get test.txt ##test.txt 是之前在 /tftpboot 目录下新建的文件
tftp>put test1.txt ##test1.txt 是在 /home 目录下新建的文件
tftp>q ##退出后,在/home目录下会有一个test.txt文件,在/tftpboot 目录下有test1.txt,表示tftp服务器安装成功!
- 配置tftpd开机启动
ps aux | grep -E 'netd|tftpd' ##查看一下是否已经有了这两个daemon进程
在开发板侧,主要设置以下几个uboot中环境变量:
setenv gatewayip 192.168.1.1 ##网关,路由器ip
setenv ipaddr 192.168.1.111 ##开发板ip,需要在路由器配置一下静态ip
setenv serverip 192.168.1.101 ##主机ip,需要在路由器配置一下静态ip
通过网口用tftp下载ramdisk镜像
ftfp ramdisk.img 0x????????
Tips:
ramdisk.img可能需要首先经过mkimage进行格式转化,否则bootm不认识
0x????????是uboot中规划的ramdisk放的地址
TODO...
TODO ...
将image放到hard disk分区:
这里只需要直接将image镜像文件放到磁盘任意分区X即可,比如我们放到分区1,比如sda1上,那么使用下面命令下载镜像。
从hard disk分区下载image到开发板:例如ext4load scsi 12:1 0x90100000 hm_uImage
- 下载命令:ext4load
- 磁盘分区:scsi 12:1,可以通过
scsi info
查看编号(12是通过scsi info打印出来的18的十六进制表示,1表示第1个分区) - 板子目标地址:0x90100000
- 镜像文件:hm_Image
通过Uboot临时下载Kernel镜像
基本过程与uboot下载ramdisk类似,这里就不再赘述了。。。
镜像启动调试
Uboot启动Kernel
Uboot阶段涉及对SOC芯片和部分外设初始化,一般最简单的情况至少需要初始化如下部分:
- uart 串口调试用
- DDR
- eMMC/Nand/Hardisk 命令bootm从这些存储设备加载kernel镜像到内存中(或者也可以通过net网络下载)
- 检测机器类型,machine-id
- 标记kernel启动参数列表或者设备树地址,atag or dtb
TODO...
TODO...
Kernel启动Android
① 在ARM64上:
从uboot通过函数boot_jump_linux()调用armv8_switch_to_el2(),再跳转到image->ep这个地址开始,
② 在ARM32上:
从uboot通过函数boot_jump_linux()调用start_kernel(),直接跳转到image->ep这个地址开始
Tips:
需要注意的是,从uboot的代码中可以看出image文件中image->ep记录了kernel代码段起始地址
我们可以通过Kernel启动中所完成的功能,简单地人为将它分成如下几个阶段:
① 硬件环境准备 - 汇编函数:_head
- 在kernel/arch/arm64/kernel/head.S
- 创建临时页表
- 开启MMU
② 软件环境准备 - C函数:start_kernel()
- 在kernel/init/main.c
- 多种初始化,比如:lock、irq/exception、clock/timer、memory、dts、vfs、scheduler等等
- 这里函数都是用__init标记放在init.section段中的,捡来会被释放掉
③ 单线程变多线程 - C函数:rest_init()
- 在kernel/init/main.c
- 启动另外2个线程:kernel_init、kthreadd
- 激活调度器
- 自身变为idle线程
④ 单核变多核 - C函数:kernel_init()/kernel_init_freeable()
- 在kernel/init/main.c
- 启动SMP多核
- 激活SMP调度
- 初始化外设驱动
- 打开/dev/console
- 创建用户态init进程
Android启动流程
Android启动流程中有几个关键点是必须成功的,这点上进程一旦启动失败,系统就会reboot重启:
① 设置default.prop中属性:
- 这里属性很重要,会影响到android系统启动的,比如ro.hardware需要通过bootargs带参数androidboot.hardware=${board_name}来带入,然后是在init.rc中解析并赋值给ro.hardware的
② 创建/data/dalvik-cache目录:
- 该目录/data/dalvik-cache是在init.rc中创建的,如果创建失败,则会导致zegote启动时创建/data/dalvik-cache/arm64失败,进而zygote启动失败
- Debug:
- 在怀疑并检查是否/data分区没有mount成功时,可以查看logcat中zygote启动失败的NativeCrash信息,进而可以手动mount /data分区、并手动mkdir创建dalvik-cache目录,然后logcat看zygote启动流程是否能进行下去
- /data分区的创建在init.$(ro.hardware).rc中,可以通过在init中开启log去查看init.$(ro.hardware).rc是否确实被import了
- 如果发现这个rc没有被import的话,一般应该是ro.hardware属性没有被设置,可以通过getprop查看,这ro.hardware是在init.cpp中从bootargs中的androidboot.hardware=xxxx解析出来的
③ 将Selinux置为permissive模式:
- 需要置为permissive模式,或者内核defconfig中关闭selinux,否则可能会因为某些进程访问某些文件的权限未配置导致启动失败
④ 加载SurfaceFlinger
- libEGL:libEGL.so库必须要有,并且版本匹配,否则surfaceflinger会启动失败
- libGLES:libGLES.so/libGLESv1_CM.so/libGLESv2.so/libGLESv3.so库必须有
TODO...
参考文档