1. 前言:
刷機,似乎是安卓手機用戶的一項專利,可是,會刷機的用戶一般都是喜新厭舊的角色。
一個系統用久了。就想換到還有一個系統。或者認為沒有原來的好,或者又認為要換回去。這樣又要重刷。
可是刷來刷去都麻煩啊,而且每次刷機也不是沒有風險的,一不小心就可能造成關鍵數據的丟失。
沒有解決的方法嗎?
有。雙系統!
甚至三系統,四系統!
!
本文就是解決問題的,而且用本文中的方法,全然能夠實現一鍵安裝,一鍵卸載系統的功能。把系統的安裝和卸載變成apk的安裝和卸載一樣簡單。
(說明下,以下的方法以三星i93xx系列的手機為例的)
2. 先來簡介下安卓系統的啟動過程:
在手機上電時,最先運行的集成到CPU芯片上的一段rom里的程序
這段程序負責載入nand flash或者sd卡上的引導程序,引導程序一般來講都是uboot
uboot會完畢一些設備的初始化,這里非常重要的部分就是nand flash,以便將linux內核載入讀到內存里並執行。
載入的內核依據情況,有可能是boot分區里的內核。也有可能是recovery分區里的內核。
內核跑起來之后,首先會掛載ramdisk到"/"根文件夾,然后運行/init創建第一個進程
init讀取/init.rc,進行進一步的初始化。完畢如創建文件夾。設置權限,掛載data,system,cache分區。啟動一系列的service。包含重要的zygote進程
zygote進程又會創建system_server進程以完畢進一步的初始化工作,並載入一系列的apk進程。
3. 接下來看下,一個安卓系統所需的分區:
一個uboot分區,負責引導內核
一個內核分區
一個system分區,用於存放安卓的系統程序和文件
一個data分區。用於存放系統的數據,apk程序。以及apk程序的數據等等。
一個cache分區。一般用於升級之用,用於保存ota升級包,升級日志等等。
4. 再按下來看下雙系統的實現方案:
4.1 內核的引導問題
這是一個比較頭痛的問題。上面講到。內核是由uboot通過boot分區或者recovery分區載入進來的
假設還要載入其他分區的內核。就要考慮改動uboot的配置參數或者代碼了
uboot的代碼我們肯定是沒有 的,
盡管一般來講uboot的配置參數往往也是保存在某個分區里的。但一般都是加密的,所以也改不了。
所以我們僅僅能考慮利用己有的分區了。
最簡單的方法就是覆蓋boot分區,將第二個安卓的boot.img寫到boot分區,
然后寫一個apk,當要啟動哪個系統時,就把哪個系統的boot.img寫到boot分區。
這樣的方式的缺點是切換麻煩,每次切換都要先啟動當中的一個系統,然后執行apk進行切換。
然后,我們把貪婪的目光瞄向了recovery分區。
大家知道,recovery分區一般在系統升級或者恢復出廠設置的時候才會用到。所以我們考慮對recovery分區進行下手。
最簡單的方法是把第二個安卓系統boot.img放到recovery分區里,這樣能夠實現觸發進recovery來引導第二個安卓系統了。
當然。也能夠在recovery分區里再放一個定制的uboot,從而實現更加靈活的載入方式。如能夠顯示引導菜單等等,再如從SD卡里載入內核等等。
可是這還是要有uboot的源代碼才行。
當然,在使用recovery分區之前,要對recovery分區作下備份。
使用recovery分區作為第二個系統的linux引導分區還有個優點就是一般手機都有開機進recovery的快捷鍵。
如三星的手機通常是在開機時同一時候按下:音量加,HOME,POWER三個按鍵就能夠進recovery.
從而實現方便的系統切換。
4.2 system,data分區的創建問題:
攻克了引導的問題。再來看下system和data分區的創建問題。
由於cache分區僅僅在升級的時候會用到,所以兩個系統能夠共用,不用再創建了。
1)又一次分區法:
也就是為每一個系統建立不同的分區,這樣的方法須要對存儲空間進行又一次划分。顯然比較麻煩,風險也比較高。可行性比較低。
2)使用虛擬磁盤的方案
大家一定對ubuntu可以在windows下直接安裝的方式印象十分深刻
實際上ubuntu可以在不又一次分區的情況下實現安裝真是利用的虛擬磁盤實現的。
相對於第一種方案。避免了又一次分區的麻煩。
虛擬磁盤是linux下非常早內核就已經支持了,是非常成熟的技術了。
所以這里虛擬磁盤是最好的選擇,而且借助於虛擬磁盤。我們不僅能夠實現雙系統,還能夠實現三系統。四系統,這全然取決於存儲空間。
5. 理論講清楚了,接下來,看下詳細怎樣干吧。
首先,我們要創建一個system虛擬磁盤,這里有兩種方法:
一種是從img直接生成虛擬磁盤,還有一種方法是要將一個ota升級包中的system分區寫到虛擬磁盤中。
第一種方法比較簡單,僅僅要運行一條命令就可以:
simg2img system.img system.disk
simg2img能夠在編譯完的out/host/linux-x86/bin/文件夾下找到
6.5 假設你拿到的是一個zip格式的ota升級包,內容類似圖中所看到的:
那么制作system虛擬磁盤就稍微有些麻煩了,看下怎樣制作:
須要改動壓縮包的META-INF\com\google\android文件夾下的update-script腳本
1)去除format和mount /system分區的腳本
format("ext4", "EMMC", "/dev/block/mmcblk0p9", "0", "/system");
mount("ext4", "EMMC", "/dev/block/mmcblk0p9", "/system"); 
        2)去除寫boot.img和data分區的腳本
package_extract_file("boot.img", "/dev/block/mmcblk0p5");
 
        
這里要千萬小心,一定要把boot.img的寫操作去掉
總之。去除一卻和system分區無關的操作,去除/system分區的格式化操作和掛載操作
這里一定要小心+細心。
3)接下來將解壓出來的ota升級包又一次打包成zip文件
4)然后進入recovery進行進一步的操作(這里recovery最好是第三方的recovery。如cm的recovery,命令比較豐富,比較好操作)
5)用usb連接手機,進入recovery后,adb會自己主動連接到recovery,然后依次運行例如以下命令:
在運行以下的腳本之前請確認你的手機能夠在shell中獲取系統權限
將升級腳本運行程序傳到手機的/data/local/tmp文件夾,update-binary在升級包里的META-INF\com\google\android文件夾,和updater-script同一個文件夾。
adb push update-binary /data/local/tmp
將改動過update-script的ota升級包傳到手機的對應文件夾
adb push ota.zip /data/local/tmp
切換到root權限
adb shell su
創建一個800M大小的虛擬磁盤
 
cd /data/local/tmp dd if=/dev/zero of=system.disk bs=1024 count=819200
loop虛擬磁盤system.disk
busybox losetup /dev/block/loop7 system.disk
對虛擬磁盤進行格式化
busybox mkfs.ext2 /dev/block/loop7
掛載虛擬磁盤到/system文件夾
busybox mount -o loop -t ext4 /dev/block/loop7 /system
改動update-binary為可運行
chmod 777 update-binary
開始運行update-script腳本,把ota升級包安裝到/system文件夾
./update-binary 2 0 ota.zip
卸載system虛擬磁盤
umount /system busybox losetup -d /dev/block/loop7
退出系統權限
exit
退出adb shell
exit
adb pull /data/local/tmp/system.disk
 
這樣,一個燒餅--system虛擬磁盤就做好了:)。
7. 接下來看下怎樣去創建一個ext4格式的data虛擬磁盤
dd if=/dev/zero of=data.disk bs=1024 count=30720 busybox losetup /dev/loop0 data.disk mkfs.ext4 -m 1 -v /dev/block/loop7
8. 接下來看下怎樣去掛載虛擬磁盤
1)先要對boot.img動手術。先要對其解壓, 網絡上應該有解壓的工具。可是我一般都喜歡自己寫一些小工具來完畢一些簡單的任務,一來加深認識。二來也方便功能的擴展。
所以,這里就用python寫了個解壓的程序:
unpack.py
"""
    unsigned char magic[BOOT_MAGIC_SIZE];
    unsigned kernel_size;  /* size in bytes */
    unsigned kernel_addr;  /* physical load addr */
    unsigned ramdisk_size; /* size in bytes */
    unsigned ramdisk_addr; /* physical load addr */
    unsigned second_size;  /* size in bytes */
    unsigned second_addr;  /* physical load addr */
    unsigned tags_addr;    /* physical addr for kernel tags */
    unsigned page_size;    /* flash page size we assume */
    unsigned unused[2];    /* future expansion: should be 0 */
    unsigned char name[BOOT_NAME_SIZE]; /* asciiz product name */
    unsigned char cmdline[BOOT_ARGS_SIZE];
    unsigned id[8]; /* timestamp / checksum / sha1 / etc */
"""
import sys
import struct
import subprocess
BOOT_MAGIC_SIZE = 8
BOOT_NAME_SIZE = 16
BOOT_ARGS_SIZE = 512
boot_img = None
kernel_img = None
ramdisk_img = None
ramdisk_cpio = None
ramdisk_out = None
if len(sys.argv) >= 2:
    boot_img = sys.argv[1]
else:
    boot_img = 'boot.img'
if len(sys.argv) >= 3:
    kernel_img = sys.argv[2]
else:
    kernel_img = 'kernel.img'
if len(sys.argv) >= 4:
    ramdisk_img = sys.argv[3]
else:
    ramdisk_img = 'ramdisk.img'
if len(sys.argv) >= 5:
    ramdisk_cpio = sys.argv[4]
else:
    ramdisk_cpio = 'ramdisk-m.cpio'
if len(sys.argv) >= 6:
    ramdisk_out = sys.argv[5]
else:
    ramdisk_out = 'ramdisk'
f = open(boot_img,'rb')
magic = f.read(BOOT_MAGIC_SIZE)
kernel_size = f.read(4)
kernel_size = struct.unpack('I',kernel_size)[0]
print kernel_size
kernel_addr = f.read(4)
kernel_addr = struct.unpack('I',kernel_addr)[0]
print "0x%x"%(kernel_addr,)
ramdisk_size = f.read(4)
ramdisk_size = struct.unpack('I',ramdisk_size)[0]
print ramdisk_size
ramdisk_addr = f.read(4)
ramdisk_addr = struct.unpack('I',ramdisk_addr)[0]
print "0x%x"%(ramdisk_addr,)
second_size = f.read(4)
second_size = struct.unpack('I',second_size)[0]
print second_size
second_addr = f.read(4)
second_addr = struct.unpack('I',second_addr)[0]
print "0x%x"%(second_addr,)
"""
    unsigned tags_addr;    /* physical addr for kernel tags */
    unsigned page_size;    /* flash page size we assume */
    unsigned unused[2];    /* future expansion: should be 0 */
"""
f.seek(BOOT_MAGIC_SIZE + 3*2*4 + 4+4+4*2)
"""
    unsigned char name[BOOT_NAME_SIZE]; /* asciiz product name */
    unsigned char cmdline[BOOT_ARGS_SIZE];
"""    
name = f.read(BOOT_NAME_SIZE)
print name
cmdline = f.read(BOOT_ARGS_SIZE)
print cmdline
page_size = 2048
f.seek(2048)
data = f.read(kernel_size)
with open(kernel_img,'wb') as ff:
    ff.write(data)
           
ramdisk_offset = 2048 + (kernel_size+page_size-1)/page_size*page_size
print 'ramdisk_offset:',ramdisk_offset
f.seek(ramdisk_offset)
data = f.read(ramdisk_size)
with open(ramdisk_img,'wb') as ff:
    ff.write(data)
cmd = 'gzip -d -r  < %s  >%s'%(ramdisk_img,ramdisk_cpio)
subprocess.check_output(cmd,shell=True)
cmd = 'mkdir %(ramdisk)s;cd %(ramdisk)s;cpio -i < ../ramdisk-m.cpio'%{'ramdisk':ramdisk_out}
subprocess.check_output(cmd,shell=True) 
         
        
 
這個程序會從boot.img中提取出kernel.img和ramdisk.img。而且默認將zip格式的ramdisk.img解壓到ramdisk文件夾
2)改動fstab.smdk4x12
原內容:
# Android fstab file. #<src> <mnt_point> <type> <mnt_flags and options> <fs_mgr_flags> # The filesystem that contains the filesystem checker binary (typically /system) cannot # specify MF_CHECK, and must come before any filesystems that do specify MF_CHECK /dev/block/mmcblk0p9 /system ext4 ro,errors=panic wait /dev/block/mmcblk0p3 /efs ext4 nosuid,nodev,noatime,noauto_da_alloc,discard,journal_async_commit,errors=panic wait,check /dev/block/mmcblk0p8 /cache ext4 nosuid,nodev,noatime,noauto_da_alloc,discard,journal_async_commit,errors=panic wait,check # data partition must be located at the bottom for supporting device encryption /dev/block/mmcblk0p12 /data ext4 nosuid,nodev,noatime,noauto_da_alloc,discard,journal_async_commit,errors=panic wait,check,encryptable=footer # VOLD /devices/platform/s3c-sdhci.2/mmc_host/mmc1/ /storage/extSdCard vfat default voldmanaged=sdcard:auto /devices/platform/s5p-ehci/usb1 /storage/UsbDriveA vfat default voldmanaged=sda:auto /devices/platform/s5p-ehci/usb1 /storage/UsbDriveB vfat default voldmanaged=sdb:auto /devices/platform/s5p-ehci/usb1 /storage/UsbDriveC vfat default voldmanaged=sdc:auto /devices/platform/s5p-ehci/usb1 /storage/UsbDriveD vfat default voldmanaged=sdd:auto /devices/platform/s5p-ehci/usb1 /storage/UsbDriveE vfat default voldmanaged=sde:auto /devices/platform/s5p-ehci/usb1 /storage/UsbDriveF vfat default voldmanaged=sdf:auto
改動后的內容:
# Android fstab file. #<src> <mnt_point> <type> <mnt_flags and options> <fs_mgr_flags> # The filesystem that contains the filesystem checker binary (typically /system) cannot # specify MF_CHECK, and must come before any filesystems that do specify MF_CHECK /dev/block/mmcblk0p3 /efs ext4 nosuid,nodev,noatime,noauto_da_alloc,discard,journal_async_commit,errors=panic wait,check /dev/block/mmcblk0p8 /cache ext4 nosuid,nodev,noatime,noauto_da_alloc,discard,journal_async_commit,errors=panic wait,check # data partition must be located at the bottom for supporting device encryption /dev/block/mmcblk0p12 /dat ext4 nosuid,nodev,noatime,noauto_da_alloc,discard,journal_async_commit,errors=panic wait,check,encryptable=footer # VOLD /devices/platform/s3c-sdhci.2/mmc_host/mmc1/ /storage/extSdCard vfat default voldmanaged=sdcard:auto /devices/platform/s5p-ehci/usb1 /storage/UsbDriveA vfat default voldmanaged=sda:auto /devices/platform/s5p-ehci/usb1 /storage/UsbDriveB vfat default voldmanaged=sdb:auto /devices/platform/s5p-ehci/usb1 /storage/UsbDriveC vfat default voldmanaged=sdc:auto /devices/platform/s5p-ehci/usb1 /storage/UsbDriveD vfat default voldmanaged=sdd:auto /devices/platform/s5p-ehci/usb1 /storage/UsbDriveE vfat default voldmanaged=sde:auto /devices/platform/s5p-ehci/usb1 /storage/UsbDriveF vfat default voldmanaged=sdf:auto
這里主要是刪除了system分區的掛載,而且把data分區由原來的掛載到dat改為掛載到/dat文件夾
由於這里我們實際上要掛載的是虛擬磁盤
3)init.rc中:
改動on init:
在
mkdir /system
后面加上:
mkdir /dat
4)init.smdk4x12.rc中,改動on fs
在
mount_all /fstab.smdk4x12
后面加上:
mount ext4 loop@/dat/system.disk /system rw wait noatime mount ext4 loop@/dat/data.disk /data wait nosuid nodev noatime
總結:
1)這里。先把應該mount到/data文件夾的分區mount到/dat文件夾
2)接下來的兩行腳本就把虛擬磁盤給分別掛載到/system和/data
3) 是的,so easy!,可是當功能還未實現時,那種從醞釀到嘗試,再到成功的過程還有有一翻體會的,盡管關鍵的虛擬分區的掛載是在三心二意的情況下完畢的(一邊看着CCTV的記錄片,哈哈,千萬不要學)。
9. 又一次把kernel和ramdisk打包成boot.img
改動好的ramdisk,接下來就要對其進行又一次打包成boot.img了,這里會用到三條命令:mkbootfs,minigzip和mkbootimg。mkbootfs把ramdisk里的全部文件打包成cpio格式的文件
minigzip再對其進行壓縮,mkbootimg從kernel和ramdisk.img生成boot.img,用法例如以下:
mkbootfs ramdisk | minigzip > ramdisk.img
mkbootimg --kernel kernel.img --ramdisk ramdisk.img --output boot.img
10. 接下來,到了最后一步了。怎樣安裝第二個Android操作系統?
安裝的工作事實上真的非常easy,把boot.img寫到recovery分區,把system.disk。data.disk復制到/data分區。
當然。也能夠把system.disk和data.disk放到外部SD卡中。這要涉及到改動ramdisk
11.最后。總結下全文吧:
在手機上實現雙系統已經不是什么新技術了,在用虛擬磁盤的方式實現雙系統之后。后來百度了下,看到11年寫的一篇文章, 是在SD卡上進行分區來實現雙系統的,操作比較麻煩,
切換還要啟動到當中一個系統通過刷boot分區來實現系統的切換,還不如本文直接刷recovery分區的方式來的方便。
本文的雙系統實現事實上還是比較簡單的:
1)將system.img通過simg2img轉換成能夠直接通過loop掛載的格式system.disk
2)創建data分區虛擬磁盤data.disk
3)改動boot.img中的啟動相關的腳本,實現掛載虛擬磁盤,並又一次打包
4)將system.disk。data.disk放到原data分區
5)將又一次打包的boot.img刷到recovery分區。
12. Is this the end or just the beginning?
假設是網絡機頂盒,上面的已經足夠了,可是我們是在手機上創建雙系統,所以仍然有一段路要走。下面是須要思考的問題:
1)怎樣保持雙系統中的通信錄的同步問題
2)怎樣同步雙系統中經常使用工具的數據,如微信聊天記錄等等。
3)怎樣處理不同版本號的android系統基帶的不兼容性問題。
4) 怎樣處理efs分區在不同版本號的android系統不相互兼容問題。
5) 怎樣實現Android+WP雙系統
6) 怎樣實現三系統,四系統?
7) ...
(完)
