2023全新高研班!脱壳机更新、iOS/eBPF、云手机...
1、课程内容新增
更新内容采用直播的方式,周末直播,1v1激情连麦!
有问必答,知无不言,言无不尽,用心服务!
更有职业推介服务,全方位简历指导/服务就业!
2、开学大礼包
现在报名送开课礼包
3W班高研网课开学大礼包:香橙派OrangePi 5开发板
2W班高研网课开学大礼包:测试手机一部-pixel 2代
讲师风采:《云手机底层技术揭密 : Android系统启动与Magisk原理》
本文为看雪论坛精华文章
看雪论坛作者ID:飞翔的猫咪
一. 磁盘分区表mbr,gpt的概念
二. ramdisk,initrd,ramfs,tmpfs,initramfs,rootfs,根文件系统名词的澄清
三. Linux内核启动init进程的五种不同情况
四. 安卓系统启动的三种不同方式
五. Magisk原理
一
磁盘分区表mbr,gpt
bootstrap code area占据446个字节,包含了启动相关的代码 partition table分区表占据了64个字节,包含了四个分区表的内容,每个分区表占据16个字节 446+64 = 510,还剩下最后两个字节的内容为0x55aa,这是MBR的标志
所以MBR这个名词不仅仅指磁盘的第一个扇区,它还暗指了上面的这种布局以及分区格式。
MBR中的分区表格式就用到了C/H/S的表示方法。
C/H/S是一种老式的逻辑地址方式,它的存在有一定的历史原因,早期磁盘的结构就是柱面,磁头和扇区这些,后面磁盘的物理结构就不一定再是这些了,CHS也就不再对应于磁盘物理上的结构,对于CHS转换到实际磁盘的地址则是磁盘控制器的工作。
新式的逻辑地址表示方法为LBA:Logical Block Addressing,它的想法很简单,就是将磁盘视为一个大的字节数组,LBA从0开始: LBA0,LBA1,..... 每一块LBA的大小通常为512字节(也有以1024字节为块大小的固态硬盘,和4096字节为块大小的flash存储设备)。
A = (c ⋅ Nheads + h) ⋅ Nsectors + (s − 1)
A为LBA地址,Nheads为磁盘上的heads个数,Nsectors是每个track里边的最大扇区个数。
理解这个公式可以简单的认为磁盘是这样构成的:
扇区是一块扇形区域,像一块切好的比萨 多个扇区最终组成一个圆,好比组成一个完整的比萨,这块比萨可以分割成多少个扇区由Nsectors表示 磁盘由多个完整的比萨组成(串在一起),数量共为Nheads个
80 01 01 00 0B FE BF FC 3F 00 00 00 7E 86 BB 00
80 --> 1字节,分区状态: 00 --> 非活动分区,80 --> 活动分区
01 01 00 --> 3字节, 共同表示分区起始C/H/S(但是并不指C=1,H=1,S=0)
0B --> 文件系统标志位 : "0B"表示分区的系统类型是FAT32,其他比较常用的有04(FAT16)、07(NTFS)
3F 00 00 00 --> 分区起始相对扇区号
7E 86 BB 00 --> 分区总的扇区数
由于MBR使用4个字节表示分区总的扇区数,因此它可以表示的最大分区大小为2199023255552字节,约为2T,这也是MBR的一个限制。
由于各种限制,MBR已经成为老式的分区方式,新式的分区方式为GPT。
GUID Partition Table
python3 -m pip install gpt
ls -l /sys/block/sd*
lrwxrwxrwx 1 root root 0 2023-01-11 03:56 /sys/block/sda -> ../devices/platform/soc/1d84000.ufshc/host0/target0:0:0/0:0:0:0/block/sda
lrwxrwxrwx 1 root root 0 2023-01-11 10:36 /sys/block/sdb -> ../devices/platform/soc/1d84000.ufshc/host0/target0:0:0/0:0:0:1/block/sdb
lrwxrwxrwx 1 root root 0 2023-01-11 10:36 /sys/block/sdc -> ../devices/platform/soc/1d84000.ufshc/host0/target0:0:0/0:0:0:2/block/sdc
lrwxrwxrwx 1 root root 0 2023-01-11 10:36 /sys/block/sdd -> ../devices/platform/soc/1d84000.ufshc/host0/target0:0:0/0:0:0:3/block/sdd
lrwxrwxrwx 1 root root 0 2023-01-11 10:36 /sys/block/sde -> ../devices/platform/soc/1d84000.ufshc/host0/target0:0:0/0:0:0:4/block/sde
lrwxrwxrwx 1 root root 0 2023-01-11 10:36 /sys/block/sdf -> ../devices/platform/soc/1d84000.ufshc/host0/target0:0:0/0:0:0:5/block/sdf
它们的逻辑地址空间是独立的,都是从LBA 0开始,因此都有各自的分区表结构。
先来看一下sda的分区表信息,对应的/dev下面的块设备文件为/dev/block/sda,看一下sda设备的块逻辑大小(内核include/linux/blkdev.h文件的bdev_logical_block_size()函数):
cat /sys/block/sda/queue/logical_block_size
4096
安卓存储设备采用的是gpt分区,LBA0仍然给MBR使用,叫做"Protective MBR",dump它的内容查看分区表:
dd if=/dev/block/sda bs=4096 count=1 > /data/local/tmp/lba0
cat lba0 | print_mbr
Warning: Using only the first 512 bytes of input
<<< MBR >>>
BootCode: 0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
UniqueMBRDiskSignature: 0x00000000
Unknown: 0x0000
PartitionRecord: 0x00000200ee00000001000000ffffffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Signature: 0xAA55
<<< MBR Partition #0 >>>
#0.BootIndicator: 0x0
#0.Is Bootable? (syn): No
#0.StartingCHS: 0, 0, 2
#0.OSType: 0xEE
#0.OSType (syn): GPT Protective
#0.EndingCHS: 0, 0, 0
#0.StartingLBA: 1
#0.SizeInLBA: 4294967295
<<< MBR Partition #1 >>>
#1.BootIndicator: 0x0
#1.Is Bootable? (syn): No
#1.StartingCHS: 0, 0, 0
#1.OSType: 0x0
#1.OSType (syn): Empty
#1.EndingCHS: 0, 0, 0
#1.StartingLBA: 0
#1.SizeInLBA: 0
<<< MBR Partition #2 >>>
#2.BootIndicator: 0x0
#2.Is Bootable? (syn): No
#2.StartingCHS: 0, 0, 0
#2.OSType: 0x0
#2.OSType (syn): Empty
#2.EndingCHS: 0, 0, 0
#2.StartingLBA: 0
#2.SizeInLBA: 0
<<< MBR Partition #3 >>>
#3.BootIndicator: 0x0
#3.Is Bootable? (syn): No
#3.StartingCHS: 0, 0, 0
#3.OSType: 0x0
#3.OSType (syn): Empty
#3.EndingCHS: 0, 0, 0
#3.StartingLBA: 0
#3.SizeInLBA: 0
dd if=/dev/block/sda bs=4096 count=1 skip=1 > /data/local/tmp/lba1
在pc运行命令
cat lba1 | print_gpt_header
Warning: Using only the first 92 bytes of input
<<< GPT Header >>>
Signature: 0x4546492050415254
Revision: 0x00000100
HeaderSize: 92
HeaderCRC32: 0x1d3ca154
HeaderCRC32 (calculated): 0x1d3ca154
Reserved: 0x00000000
MyLBA: 1
AlternateLBA: 15589375
FirstUsableLBA: 6
LastUsableLBA: 15589370
PartitionEntryLBA: 2
NumberOfPartitionEntries: 21
SizeOfPartitionEntry: 128
PartitionEntryArrayCRC32: 0xd171d8a9
dd if=/dev/block/sda bs=4096 count=32 skip=2 > /data/local/tmp/lba2-33
cat lba2-33| print_gpt_partition_entry_array
<<< GPT Partition Entry #4 >>>
#4.PartitionTypeGUID: 0x11b0d797da543548b3c4917ad6e73d74
#4.PartitionTypeGUID (syn): 97d7b011-54da-4835-b3c4-917ad6e73d74
#4.PartitionType (syn): ?
#4.UniquePartitionGUID: 0xdbfff6623331984e9b30f325067783f0
#4.UniquePartitionGUID (syn): 62f6ffdb-3133-4e98-9b30-f325067783f0
#4.StartingLBA: 520
#4.EndingLBA: 721415
#4.Attributes: 0x5f000000000000
#4.Attributes (syn): []
#4.PartitionName: 0x730079007300740065006d005f0061000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
#4.PartitionName (syn): system_a
<<< GPT Partition Entry #5 >>>
#5.PartitionTypeGUID: 0xd46c0377d503bb428ed137e5a88baa34
#5.PartitionTypeGUID (syn): 77036cd4-03d5-42bb-8ed1-37e5a88baa34
#5.PartitionType (syn): ?
#5.UniquePartitionGUID: 0x75465b2326a94d4e9c32e9acf66da87d
#5.UniquePartitionGUID (syn): 235b4675-a926-4e4d-9c32-e9acf66da87d
#5.StartingLBA: 721416
#5.EndingLBA: 1442311
#5.Attributes: 0x0
#5.Attributes (syn): []
#5.PartitionName: 0x730079007300740065006d005f0062000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
#5.PartitionName (syn): system_b
system_a,system_b是A/B分区的概念。对于高版本的安卓来说,上面的分区表里是没有recovery分区的。
bootloader会分析gpt分区表结构,当执行fastboot flash system_a system.img命令的时候bootloader才知道要刷写的分区位置。
Linux内核也会扫描gpt分区表,生成gendisk的分区表相关的数据结构,以高通的ufs为例,它的调用过程为:
drivers/scsi/sd.c:sd_probe()
sd_probe_async()
block/genhd.c:device_add_disk()
register_disk()
fs/block_dev.c:blkdev_get()
__blkdev_get()
block/partition-generic.c:rescan_partitions()
/sys/block/sda/sda1,/sys/block/sda/sda2,/sys/block/sda/sda3 ...
ls -l /dev/block/platform/soc/1d84000.ufshc/by-name
lrwxrwxrwx 1 root root 15 1970-08-22 11:05 ALIGN_TO_128K_1 -> /dev/block/sdd1
lrwxrwxrwx 1 root root 15 1970-08-22 11:05 ALIGN_TO_128K_2 -> /dev/block/sdf1
lrwxrwxrwx 1 root root 16 1970-08-22 11:05 ImageFv -> /dev/block/sdf14
lrwxrwxrwx 1 root root 15 1970-08-22 11:05 abl_a -> /dev/block/sde4
lrwxrwxrwx 1 root root 16 1970-08-22 11:05 abl_b -> /dev/block/sde16
lrwxrwxrwx 1 root root 15 1970-08-22 11:05 aop_a -> /dev/block/sde1
lrwxrwxrwx 1 root root 16 1970-08-22 11:05 aop_b -> /dev/block/sde13
lrwxrwxrwx 1 root root 16 1970-08-22 11:05 apdp_a -> /dev/block/sda15
lrwxrwxrwx 1 root root 16 1970-08-22 11:05 apdp_b -> /dev/block/sda16
二
ramdisk,initrd,ramfs,tmpfs,initramfs,rootfs,根文件系统名词的澄清
虽然安卓的boot.img里边也有个文件叫ramdisk,但是其实它用的技术本质上却并不是ramdisk,后面会有详细描述。
开启ramdisk功能需要配置CONFIG_BLK_DEV_RAM=y,同时关联的配置为CONFIG_BLK_DEV_RAM_COUNT默认为16,表示ramdisk设备的个数,CONFIG_BLK_DEV_RAM_SIZE默认为8192,以1kb为单位的ramdisk设备的字节数大小。
当ramdisk功能开启以后会有如下的设备文件:/dev/ram0,/dev/ram1,/dev/ram2 ... /dev/ram15,由于每一块模拟的都是硬盘,因此可以直接格式化为指定文件系统并挂载:
sudo mkfs.ext4 /dev/ram0
sudo mount /dev/ram0 mount_dir
cat /proc/filesystems
nodev ramfs
nodev tmpfs
nodev rootfs
rootfs虽然它直译过来是"根文件系统"的意思,但这里指的是内核中的一个文件系统,它和用户空间的"根文件系统"并不是一个东西:
struct file_system_type rootfs_fs_type = {
.name = "rootfs",
.init_fs_context = rootfs_init_fs_context,
.kill_sb = kill_litter_super,
};
三
Linux内核启动init进程的五种不同情况
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.89.tar.xz
tar xvf linux-5.15.89.tar.xz
cd linux-5.15.89
#默认配置就为x86_64的配置
make defconfig
#编译出x86_64的内核镜像
make bzImage
qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage
qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage --append "console=ttyS0" -nographic
Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)
启动方式1:
mkdir -p my_rootfs/root_disk
cd my_rootfs/root_disk
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Hello, Linux!!!\n");
sleep(999999);
return 0;
}
gcc -static init.c -o init
dd if=/dev/zero of=disk.img bs=10M count=1
sudo mkfs.ext4 disk.img
mkdir mount_dir
sudo mount disk.img mount_dir
sudo cp init mount_dir
sudo umount mount_dir
qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage -hda my_rootfs/root_disk/disk.img --append "root=/dev/sda init=/init console=ttyS0" -nographic
这种启动方式有什么缺点呢?旧时期这种方式足以启动满足要求,但是随着时代的发展,硬件变的越来越复杂,根文件系统可能处于各种scsi,sata,flash设备上,甚至RAID阵列,可插拔的usb设备中。根文件系统还可能被压缩和加密,那么如何解压缩,如何解密则成了问题。
启动方式2:
make menuconfig
选中以后.config增加的配置项为:
CONFIG_BLK_DEV_RAM=y
CONFIG_BLK_DEV_RAM_COUNT=16
CONFIG_BLK_DEV_RAM_SIZE=4096
mkdir -p my_rootfs/old_ramdisk
cd my_rootfs/old_ramdisk
#include <stdio.h>
#include <unistd.h>
int main() {
printf("From linuxrc : Hello, Linux!!\n");
return 0;
}
gcc -static linuxrc.c -o linuxrc
dd if=/dev/zero of=ramdisk.img bs=2M count=1
sudo mkfs.ext2 ramdisk.img
mkdir mount_dir
sudo mount ramdisk.img mount_dir
sudo cp linuxrc mount_dir
cd mount_dir
sudo mkdir dev
cd dev
sudo mknod console c 5 1
sudo umount mount_dir
qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage -hda my_rootfs/root_disk/disk.img -initrd my_rootfs/old_ramdisk/ramdisk.img --append "root=/dev/sda init=/init console=ttyS0" -nographic
会先打印出"From linuxrc : Hello, Linux!!"
接着打印出"Hello, Linux!!!"
第一种启动方式中内核执行完/init进程以后就不回头了,init进程如果退出内核会panic。
而第二种启动方式内核会利用ramdisk在上挂载ramdisk.img并执行/linuxrc程序(写死的),并且等待这个程序的返回,然后内核再去挂载并执行位于/dev/sda中的init程序。linuxrc执行的任务一般是加载下一阶段init程序所需要的模块。
启动方式3:
mkdir -p my_rootfs/initrd
cd my_rootfs/initrd
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Hello, Linux!!!\n");
sleep(999999);
return 0;
}
gcc -static init.c -o init
创建一个2M大小的镜像文件:
dd if=/dev/zero of=disk.img bs=2M count=1
格式化为ext2文件系统:
sudo mkfs.ext2 disk.img
挂载这个文件系统:
mkdir mount_dir
sudo mount disk.img mount_dir
将init程序拷贝到mount_dir中:
sudo cp init mount_dir
创建/dev/console设备节点不然不会有日志输出:
cd mount_dir
sudo mkdir dev
cd dev
sudo mknod console c 5 1
退出目录并卸载文件系统:
sudo umount mount_dir
用它来启动:
qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage -initrd my_rootfs/initrd/disk.img --append "root=/dev/ram0 init=/init console=ttyS0" -nographic
会打印出"Hello, Linux!!!"
这种启动方式不再需要指定-hda参数,只指定了一个-initrd参数,且root修改为/dev/ram0。
启动方式4:
mkdir -p my_rootfs/initrd_cpio/out
cd my_rootfs/initrd_cpio
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Hello, Linux!!!\n");
sleep(999999);
return 0;
}
gcc -static init.c -o init
cp init out/
cd out
find . | cpio -o -H newc | gzip > ../simple_initrd.cpio.gz
qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage -initrd my_rootfs/initrd_cpio/simple_initrd.cpio.gz --append "init=/init console=ttyS0" -nographic
上面的过程是不需要root权限的,这也是这种启动方式的一个小优点。
安卓boot.img中的ramdisk启动算是此类启动方式。
启动方式5:
mkdir my_rootfs/initramfs/out
cd my_rootfs/initramfs
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Hello, Linux!!!\n");
sleep(999999);
return 0;
}
gcc -static init.c -o init
cp init out/
cd out
mkdir dev
cd dev
sudo mknod console c 5 1
cd ..
find . | cpio -o -H newc | gzip > ../initramfs_data.cpio.gz
make menuconfig
my_rootfs/initramfs/initramfs_data.cpio.gz
CONFIG_INITRAMFS_SOURCE="my_rootfs/initramfs/initramfs_data.cpio.gz"
重新编译:
make bzImage
qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage --append "init=/init console=ttyS0" -nographic
源码分析:
内核的c语言函数入口为start_kernel:
start_kernel()
vfs_caches_init()
mnt_init()
init_rootfs()
init_mount_tree()
vfs_kern_mount(&rootfs_fs_type, 0, "rootfs", NULL)
arch_call_rest_init()
rest_init()
kernel_thread(kernel_init, NULL, CLONE_FS)
kernel_init_freeable()
do_basic_setup()
do_initcalls() --> rootfs_initcall(populate_rootfs)
do_populate_rootfs()
unpack_to_rootfs()
#ifdef CONFIG_BLK_DEV_RAM
populate_initrd_image(err);
console_on_rootfs()
if (init_eaccess(ramdisk_execute_command) != 0) prepare_namespace()
initrd_load()
mount_root()
create_dev("/dev/root", ROOT_DEV)
mount_block_root("/dev/root", root_mountflags)
devtmpfs_mount();
init_mount(".", "/", NULL, MS_MOVE, NULL);
init_chroot(".");
try_to_run_init_process()
set_fs_pwd(current->fs, &root);
set_fs_root(current->fs, &root);
void __init init_rootfs(void)
{
if (IS_ENABLED(CONFIG_TMPFS) && !saved_root_name[0] &&
(!root_fs_names || strstr(root_fs_names, "tmpfs")))
is_tmpfs = true;
}
接下来的流程会经由do_basic_setup()初始化驱动以后调用到populate_rootfs()函数,从而调用到do_populate_rootfs()函数,在这个函数首先会调用unpack_to_rootfs(__initramfs_start, __initramfs_size)将initramfs的内容解压至rootfs,以上的过程对所有启动过程都是相同的,接下来针对上面的不同启动方式进行分析。
qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage -hda my_rootfs/root_disk/disk.img --append "root=/dev/sda init=/init console=ttyS0" -nographic
回到kernel_init_freeable()函数中进行到如下的判断:
if (init_eaccess(ramdisk_execute_command) != 0) {
ramdisk_execute_command = NULL;
prepare_namespace();
}
在prepare_namespace()函数中root_device_name的值为/dev/sda,首先会获取/dev/sda的主设备和次设备号表示dev_t ROOT_DEV,接着将root_device_name从/dev/sda截取为sda,然后调用initrd_load()函数加载/initrd.image文件,由于rootfs中没有/initrd.image因此initrd_load除了创建了设备节点/dev/ram并不会做什么事情。
接下执行:
//创建ROOT_DEV对应的设备节点/dev/root,如果没有指定rootfstype命令行参数就尝试遍历文件系统类型对/dev/root进行挂载,挂载点为/root,并且调用init_chdir("/root")将工作目录切换到/root目录下
mount_root();
//将当前工作目录(/root)移动挂载至/目录下
init_mount(".", "/", NULL, MS_MOVE, NULL);
//切换当前进程的根目录至当前目录
init_chroot(".");
qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage -hda my_rootfs/root_disk/disk.img -initrd my_rootfs/old_ramdisk/ramdisk.img --append "root=/dev/sda init=/init console=ttyS0" -nographic
但由于ramdisk.img的格式并不是cpio,而是ext2镜像,unpack_to_rootfs()函数会失败:"rootfs image is not initramfs (invalid magic at start of compressed archive);looks like an initrd",并进入到populate_initrd_image()函数。
回到kernel_init_freeable()函数仍然会进入到prepare_namespace函数并且调用initrd_load函数:
if (rd_load_image("/initrd.image") && ROOT_DEV != Root_RAM0) {
init_unlink("/initrd.image");
handle_initrd();
return true;
}
rd_load_image的逻辑是先尝试识别出/initrd.image文件的格式,由于ramdisk.img的格式是ext2,因此会打印出"RAMDISK:ext2 filesystem found at block 0"表示识别出是ext2的文件格式。接下来将/initrd.image文件拷贝至ramdisk设备文件/dev/ram中。
这个函数总体逻辑是创建表示Root_RAM0的/dev/root.old设备并挂载然后执行其上的/linuxrc程序并等待其返回,接着调用mount_root继续/dev/sda的挂载。
if (initrd_load())
goto out;
qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage -initrd my_rootfs/initrd/disk.img --append "root=/dev/ram0 init=/init console=ttyS0" -nographic
if (rd_load_image("/initrd.image") && ROOT_DEV != Root_RAM0) {
init_unlink("/initrd.image");
handle_initrd();
return true;
}
启动分为两个过程:rootfs --> ramdisk initrd, 它本质上也是ramdisk技术的应用。
qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage -initrd my_rootfs/initrd_cpio/simple_initrd.cpio.gz --append "init=/init console=ttyS0" -nographic
由于simple_initrd.cpio.gz的格式为cpio gzip压缩格式,所以unpack_to_rootfs就会成功就不会进入ramdisk逻辑相关的populate_initrd_image()函数中了,并且释放掉initrd占据的物理内存。
if (init_eaccess(ramdisk_execute_command) != 0) {
ramdisk_execute_command = NULL;
prepare_namespace();
}
qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage --append "init=/init console=ttyS0" -nographic
这种启动是将cpio压缩包和内核编译在了一起,启动过程步骤:rootfs --> initramfs。
四
安卓系统启动的三种不同方式
可以先参考一下magisk的文档:
https://topjohnwu.github.io/Magisk/boot.html
安卓的启动流程相当复杂并且一直在演进,演进的是最终目标都是为了提升用户体验、解决碎片化问题并且提升安全性。magisk把安卓启动方式归为三类:
Method Initial rootdir Final rootdir
A rootfs rootfs
B system system
C rootfs system
Method B为system-as-root,两阶段都是system。
Method C为一阶段为rootfs,二阶段为system。
Method A:
cache.img --> 刷入cache分区
recovery.img --> 刷入recovery分区
system.img --> 刷入system分区
userdata.img --> 刷入userdata分区,最终挂载为/data
radio-hammerhead-m8974a-2.0.50.1.16.img --> 刷入radio分区
bootloader-hammerhead-hhz11k.img --> 刷入bootloader分区
android不仅需要bootloader遵循arm/arm64平台上linux的boot协议:
https://www.kernel.org/doc/Documentation/arm/Booting
https://www.kernel.org/doc/Documentation/arm64/booting.txt
https://source.android.com/docs/core/architecture/bootloader/boot-image-header?hl=zh-cn
struct boot_img_hdr
{
uint8_t magic[BOOT_MAGIC_SIZE];
uint32_t kernel_size; /* size in bytes */
uint32_t kernel_addr; /* physical load addr */
uint32_t ramdisk_size; /* size in bytes */
uint32_t ramdisk_addr; /* physical load addr */
uint32_t second_size; /* size in bytes */
uint32_t second_addr; /* physical load addr */
uint32_t tags_addr; /* physical addr for kernel tags */
uint32_t page_size; /* flash page size we assume */
uint32_t unused;
uint32_t os_version;
uint8_t name[BOOT_NAME_SIZE]; /* asciiz product name */
uint8_t cmdline[BOOT_ARGS_SIZE];
uint32_t id[8]; /* timestamp / checksum / sha1 / etc */
uint8_t extra_cmdline[BOOT_EXTRA_ARGS_SIZE];
};
make unpack_bootimg
out/host/linux-x86/bin/unpack_bootimg --boot_img boot.img --out boot_out
通过aosp编译hammerhead的boot.img时,zImage-dtb的来源为device/lge/hammerhead-kernel/zImage-dtb,当然也可以自己编译内核,替换掉device/lge/hammerhead-kernel/zImage-dtb再编译boot.img。
mv ramdisk ramdisk.img.gz
gunzip ramdisk.img.gz
mkdir ramdisk_dir
cd ramdisk_dir
cpio -i -F ../ramdisk.img
由于ramdisk的格式为gzip cpio,所以对于android 4.4的nexus5来说,所使用的启动方式正是上面描述的启动方式4,对于qemu来说直接指定-initrd参数即可,那么nexus5的bootloader是通过什么方式将initrd传递给内核的呢?
start_kernel:
setup_arch()
setup_machine_fdt()
of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
early_init_dt_scan_chosen()
early_init_dt_check_for_initrd()
early_init_dt_setup_initrd_arch()
rootfs / rootfs ro,relatime 0 0
Method B:
既然boot.img中已经不需要ramdisk了,那何不去掉recovery分区把recovery的ramdisk放在boot中?
android 7引入了一个新的编译配置变量BOARD_USES_RECOVERY_AS_BOOT,当它为true时会指示编译系统将编译出来的ramdisk-recovery.img放在boot.img中替换掉原来的ramdisk。
下载aosp 7.1.2和pixel1厂商相关包编译后out/target/product/sailfish下会有ramdisk-recovery.img,boot.img文件,解压boot.img以后查看里边的ramdisk确实是和ramdisk-recovery.img是相同的文件。
file system.img
system.img: Android sparse image, version: 1.0, Total of 524288 4096-byte output blocks in 4409 input chunks.
out/host/linux-x86/bin/simg2img system.img system_ext4.img
sudo mount system_ext4.img system_mount
root=/dev/dm-0 dm="system none ro,0 1 android-verity /dev/sda34" rootwait skip_initramfs init=/init
static int __initdata do_skip_initramfs;
static int __init skip_initramfs_param(char *str)
{
if (*str)
return 0;
do_skip_initramfs = 1;
return 1;
}
__setup("skip_initramfs", skip_initramfs_param);
if (do_skip_initramfs) {
if (initrd_start)
free_initrd();
return default_rootfs();
}
ls -l /sys/block/dm-0/slaves
lrwxrwxrwx 1 root root 0 2017-01-13 09:40 sda34 -> ../../../../soc/624000.ufshc/host0/target0:0:0/0:0:0:0/block/sda/sda34
具体的逻辑在system/core/adb/remount_service.cpp的remount_partition()函数中,由于现在/proc/mounts无法体现出/所在设备情况:
sailfish:/ # cat /proc/mounts
rootfs / rootfs rw,seclabel 0 0
/dev/root / ext4 ro,seclabel,relatime,data=ordered 0 0
#<src> <mnt_point> <type> <mnt_flags and options> <fs_mgr_flags>
/dev/block/bootdevice/by-name/system / ext4 ro,barrier=1,discard wait,slotselect,verify
Method C:
cat /proc/device-tree/chosen/linux,initrd-start | xxd
00000000: 0000 0000 8470 0000 .....p..
cat /proc/device-tree/chosen/linux,initrd-end | xxd
00000000: 0000 0000 8515 3be9
而编译出来的ramdisk-recovery.img大小也正是10828777字节。
cc_binary {
name: "init_second_stage",
recovery_available: true,
stem: "init",
defaults: ["init_defaults"],
static_libs: ["libinit"],
required: [
"e2fsdroid",
"mke2fs",
"sload_f2fs",
"make_f2fs",
],
srcs: ["main.cpp"],
symlinks: ["ueventd"],
target: {
recovery: {
cflags: ["-DRECOVERY"],
exclude_shared_libs: ["libbinder", "libutils"],
},
},
ldflags: ["-Wl,--rpath,/system/${LIB}/bootstrap"],
}
Shared library support in recovery mode
In Android 10, shared libraries are available in the recovery partition, which eliminates the need for all recovery mode executables to be static. The shared libraries are located under the /system/lib (or /system/lib64 for 64-bit devices) directory in the partition.
To add a new shared library to the recovery partition, add recovery_available: true or recovery: true to Android.bp of the shared library. The former installs the library to both the system and recovery partitions, while the latter installs it only to the recovery partition.
Shared library support can't be built with Android's make-based build system. To convert an existing static executable for the recovery mode to a dynamic one, remove LOCAL_FORCE_STATIC_EXECUTABLE := true in Android.mk or static_executable: true (in Android.bp ).
FirstStageMain(){
....
if (ForceNormalBoot()) {
mkdir("/first_stage_ramdisk", 0755);
// SwitchRoot() must be called with a mount point as the target, so we bind mount the
// target directory to itself here.
if (mount("/first_stage_ramdisk", "/first_stage_ramdisk", nullptr, MS_BIND, nullptr) != 0) {
LOG(FATAL) << "Could not bind mount /first_stage_ramdisk to itself";
}
SwitchRoot("/first_stage_ramdisk");
}
....
if (!DoFirstStageMount()) {
LOG(FATAL) << "Failed to mount required partitions early ...";
}
...
const char* path = "/system/bin/init";
const char* args[] = {path, "selinux_setup", nullptr};
execv(path, const_cast<char**>(args));
...
}
bool ForceNormalBoot() {
std::string cmdline;
android::base::ReadFileToString("/proc/cmdline", &cmdline);
return cmdline.find("androidboot.force_normal_boot=1") != std::string::npos;
}
SwitchRoot是通过move bind和chroot来实现切换根操作的。
drwxrwxr-x 4096 7月 18 2022 avb
-rw-rw-r-- 2015 7月 18 2022 fstab.sdm845
PRODUCT_COPY_FILES += \
device/google/crosshatch/fstab.hardware:$(TARGET_COPY_OUT_RECOVERY)/root/first_stage_ramdisk/fstab.$(PRODUCT_PLATFORM)
首先寻找挂载所需fstab文件,文件内容可以通过设备树传递,对于pixel3xl来说对应的路径为/first_stage_ramdisk/fstab.sdm845,只有标记为first_stage_mount的挂载点才会在这一阶段处理,以下是具有first_stage_mount标记的项:
# Android fstab file.
#<src> <mnt_point> <type> <mnt_flags and options> <fs_mgr_flags>
system /system ext4 ro,barrier=1 wait,slotselect,avb=vbmeta,logical,first_stage_mount
vendor /vendor ext4 ro,barrier=1 wait,slotselect,avb,logical,first_stage_mount
product /product ext4 ro,barrier=1 wait,slotselect,avb,logical,first_stage_mount
/dev/block/by-name/metadata /metadata ext4 noatime,nosuid,nodev,discard,sync wait,formattable,first_stage_mount
bool FirstStageMount::DoFirstStageMount() {
if (!IsDmLinearEnabled() && fstab_.empty()) {
// Nothing to mount.
LOG(INFO) << "First stage mount skipped (missing/incompatible/empty fstab in device tree)";
return true;
}
if (!InitDevices()) return false;
if (!CreateLogicalPartitions()) return false;
if (!MountPartitions()) return false;
return true;
}
触发uevent事件的逻辑是遍历/sys/class,/sys/block,/sys/devices目录下的uevent文件,向其中写入"add\n",并且利用netlink机制获取内核传递过来的uevent消息。
/dev/block/platform/soc/1d84000.ufshc/by-name/system_a -> /dev/block/sda5
/dev/block/by-name/system_a -> /dev/block/sda5
/dev/block/platform/soc/1d84000.ufshc/sda5 -> /dev/block/sda5
BOARD_SUPER_PARTITION_METADATA_DEVICE := system
[ 1.108482] init: [libfs_mgr]Created logical partition system_a on device /dev/block/dm-0
[ 1.109118] init: [libfs_mgr]Created logical partition vendor_a on device /dev/block/dm-1
[ 1.109711] init: [libfs_mgr]Created logical partition product_a on device /dev/block/dm-2
ls -l /sys/block/dm-0/slaves/
sda5 -> ../../../../platform/soc/1d84000.ufshc/host0/target0:0:0/0:0:0:0/block/sda/sda5
ls -l /sys/block/dm-1/slaves/
sda5 -> ../../../../platform/soc/1d84000.ufshc/host0/target0:0:0/0:0:0:0/block/sda/sda5
ls -l /sys/block/dm-2/slaves/
sda5 -> ../../../../platform/soc/1d84000.ufshc/host0/target0:0:0/0:0:0:0/block/sda/sda5
init: [libfs_mgr]__mount(source=/dev/block/dm-3,target=/system,type=ext4)=0: Success
init: [libfs_mgr]__mount(source=/dev/block/dm-4,target=/vendor,type=ext4)=0: Success
init: [libfs_mgr]__mount(source=/dev/block/dm-5,target=/product,type=ext4)=0: Success
https://android.googlesource.com/platform/system/core/+/a9a3b73163fda5abf237cc0f0cee97ff33e6254d/fs_mgr/README.overlayfs.md
可以看到安卓系统的启动比起linux的启动复杂了很多,它对bootloader有很多的要求,不过一旦满足了安卓的启动要求,后续的系统升级将会越来越简单。
五
magisk原理
不过这种设备我还没有遇到过,不管怎么样,先从代码层次了解一下magisk的原理吧。
app_init() {
mount_partitions
RAMDISKEXIST=false
check_boot_ramdisk && RAMDISKEXIST=true
get_flags
run_migrations
SHA1=$(grep_prop SHA1 $MAGISKTMP/config)
check_encryption
}
Magisk作者称谷歌角度的System-as-root为Legacy System-as-root。
grep ' / ' /proc/mounts | grep -q '/dev/root'
patch执行的脚本为boot_patch.sh,解压完boot.img以后提取出里边的ramdisk文件做如下处理:
./magiskboot cpio ramdisk.cpio \
"add 0750 $INIT magiskinit" \ #将编译出来的magiskinit替换掉里边的init程序
"mkdir 0750 overlay.d" \ #创建出overlay.d目录
"mkdir 0750 overlay.d/sbin" \ #创建出overlay.d/sbin目录
"$SKIP32 add 0644 overlay.d/sbin/magisk32.xz magisk32.xz" \ #将magisk32守护进程执行文件压缩包拷贝至overlay.d/sbin目录
"$SKIP64 add 0644 overlay.d/sbin/magisk64.xz magisk64.xz" \ #将magisk64守护进程执行文件压缩包拷贝至overlay.d/sbin目录
"add 0644 overlay.d/sbin/stub.xz stub.xz" #stub.xz文件添加至overlay.d/sbin,stub.xz是stub.apk的压缩包用于隐藏magisk app的功能
"patch" \ #如果不需要保留验证启动的功能则将fstab中dm-verity相关项(如verifyatboot)去掉,如果不需要保留分区加密功能则将fstab中分区加密相关项(如forceencrypt)去掉
"backup ramdisk.cpio.orig" \ #主要备份的是init程序
"mkdir 000 .backup" \
"add 000 .backup/.magisk config" #将配置项保存在.backup/.magisk文件中,里边的内容如下示:
KEEPVERITY=true
KEEPFORCEENCRYPT=true
PATCHVBMETAFLAG=false
RECOVERYMODE=false
SHA1=ea36c0b1d697814f99d38984d720875274bb1764
谢"残页"提醒,patch dtb其实是因为三星手机的特殊行为:https://github.com/topjohnwu/Magisk/pull/4788
#Remove Samwqsung RKP
./magiskboot hexpatch kernel \
49010054011440B93FA00F71E9000054010840B93FA00F7189000054001840B91FA00F7188010054 \
A1020054011440B93FA00F7140020054010840B93FA00F71E0010054001840B91FA00F7181010054
# Remove Samsung defex
# Before: [mov w2, #-221] (-__NR_execve)
# After: [mov w2, #-32768]
./magiskboot hexpatch kernel 821B8012 E2FF8F12
./magiskboot hexpatch kernel \
736B69705F696E697472616D667300 \
77616E745F696E697472616D667300
不同的启动流程如下:
// This will also mount /sys and /proc
load_kernel_info(&config);
if (config.skip_initramfs)
init = new LegacySARInit(argv, &config);
else if (config.force_normal_boot)
init = new FirstStageInit(argv, &config);
else if (access("/sbin/recovery", F_OK) == 0 || access("/system/bin/recovery", F_OK) == 0)
init = new RecoveryInit(argv, &config);
else if (check_two_stage())
init = new FirstStageInit(argv, &config);
else
init = new RootFSInit(argv, &config);
Type I:
2.如果要启动的是recovery模式,由于recovery分区并未被magisk修改,直接从recovery分区启动即可。
Type II:
2.如果要启动的是recovery模式,不会存在skip_initramfs参数,但由于boot.img中的ramdisk是ramdisk-recovery.img,所以会有/sbin/recovery或者/system/bin/recovery文件,进入到init = new RecoveryInit(argv, &config)。
Type III:
1.如果要启动的是正常系统,直接进入无magisk的原始系统。
2.如果要启动的是recovery模式,会导致magisk init执行,magisk init读取/.backup/.magisk配置文件得到RECOVERYMODE=true知道它自己是在recovery分区启动,它会调用check_key_combo()判断是否长按音量键上,如果长按则进入magisk系统,否则进入到原来的recovery系统。进入magisk系统仍然进入到init = new LegacySARInit(argv, &config);进入recovery模式会进入init = new RecoveryInit(argv, &config)。
Type IV:
2.如果要启动的是recovery模式,由于存在/system/bin/init文件。仍然会进入到init = new FirstStageInit(argv,&config)
# once everything is setup, no need to modify /
mount rootfs rootfs / ro remount
if (xmount("/dev/root", "/system_root", "ext4", MS_RDONLY, nullptr))
magisk对系统的修改主要是为了能启动magisk root管理守护进程,这需要修改init.rc文件,加入magisk的service,添加的片段见文件native/src/init/magiskrc.inc。由于selinux的存在,还需要修改selinux的策略。
安卓启动还有很多功能如avb,磁盘加密等这里没有提及,且随着安卓系统的演进会有更多新的技术加入进来,不过总体的原理是一样的。
看雪ID:飞翔的猫咪
https://bbs.kanxue.com/homepage-607812.htm
一、课程目录(内容增加)
二、服务对象
三、服务内容
上述列出的两大计划、各八大专题及其包含的二十四个细目; 专属班主任,敦促学习、鼓励士气;良好的抱团学习的氛围;
可以参加《安卓高级研修班》线下班,鼓励线下交流,与大佬谈笑风生;
注意2W班和3W班是完全独立噢,没有交集;
四、培训价格
就业班附带包就业服务(须达到合同规定的毕业标准),签合同保证就业及薪资,达不到退全款;
就业班有入学考核,缴费成功后进入考核流程,考核不通过退全款;
考核流程会包括简历筛选、班主任和老师(电话)面试等环节;
强化班仅去除包就业服务,并且无入学考核,其余与就业班完全相同;
就业班与强化班一起授课,合计35人一个班,教学上不做任何区分。
《安卓高级研修班》全系列无任何金融计划,纯预付;无任何金融套路。
网络课程为虚拟商品,购买之前可以观看下述试看内容,购买成功之后不接受退款。
五、看雪安卓应用安全能力认证
六、报名方式
网课月薪三万计划:
https://www.kanxue.com/book-brief-84.htm
七、试看地址
3W:《ida trace分析非标准算法》
3W:《Fart&frida》
3w班、2w班课程顾问微信:r0ysue(备注“安卓3w班”或“安卓2w班”)
1w班课程顾问微信:kanxuecom(备注“安卓1w班”)
免责条款
以上所有宣传稿件内容均不作为服务承诺,最终以实际签订培训合同为准。
课程大纲与细目会根据教学反馈不断优化、调整与更新,实际授课可能与宣传主题略有不同;
Q:网课内容与线下班内容一样么?
A:其实推荐两个班一起报,有好几位大佬就是两个班全报的。因为首先价格真心不贵,其实我们会将直播的时间错开,方便大家同时进修三万和两万计划,学习自己想要学习的、心仪的知识。
# 十一月
《使用frida-net脱离pc在手机上直接暴漏app的算法供三方调用》《Frida分析违法应用Native层算法》《Frida实战:一次违法应用的破解尝试》《使用unidbg破解孤挺花字符串混淆并修复so》《破解某抢票软件的VPN抓包》《从SSL库的内存漫游开发dump自定义客户端证书的通杀脚本》# 十月
《dexvmp后的算法逆向分析和还原》
《使用unicorn对ollvm字符串进行解密》
《Frida追踪定Socket接口自吐游戏APK的服务器IP和地址》
Frida hook Java/Native与init_array 自吐最终方案 》
# 九月
《macOS安装调试llvm入门》
《fart的理解和分析过程》
《使用ollvm自定义简单的字符串加密》
《使用ida trace来还原ollvm混淆的非标准算法》
# 八月
ollvm算法还原案例分享# 七月
frida跟踪应用中所有运行在解释模式的java函数# 六月
从三道题目入手入门frida
单纯使用Frida书写类抽取脱壳工具的一些心路历程和实践
某聊天app的音视频通话逆向
# 五月
初试IDA&FRIDA联合调试简单ollvm保护的加密函数源码
ollvm算法还原案例分享# 四月
某抽取壳的原理简析
frida辅助脱壳
# 三月
报 名 地 址
网课月薪三万计划:
https://www.kanxue.com/book-brief-84.htm
扫码立即报名!
课程顾问微信:r0ysue(备注“安卓高研网课”)
球分享
球点赞
球在看
点击“阅读原文”,了解更多!