☰
从PGPT分区表自动生成MTK线刷scatter的Python方案
2026/10/5 3:46:15 网站建设 项目流程

前阵子一个朋友送了我一台MTK平台的安卓手机,成色不错,但系统被他刷得只剩fastboot。正常情况下去找官方线刷包就能救,可这台机器比较冷门,网上翻遍都找不到带scatter的现成资源。后来我把他之前随手备份的一个PGPT分区表镜像拿出来,用Python花了半小时解析,直接生成了一份可用的MTK线刷引导文件(scatter),刷机一次通过。这事之后我就想,与其让大家继续卡在“没有scatter”上,不如把这套完整流程写出来。

下面这套流程会从PGPT分区表的原理讲起,教你如何在手机还能开机、还能root的时候把PGPT备份出来,然后用一个纯Python脚本解析分区信息,自动生成SP Flash Tool需要的线刷引导文件,最后附上我自己踩过的坑以及校验方法。不管你是第一次接触MTK刷机的新手,还是被各种半吊子工具坑过多次的老手,这套流程应该都能帮上忙。

1. 先搞清楚:PGPT在MTK手机上到底有多重要

1.1 一次误刷分区,我把备用机刷成砖之后

先说一个我自己的真实案例。去年打算给一台MT6765的老手机刷入第三方REC,结果在刷写时用了一个不匹配的scatter,把原本在userdata开头的分区写到了system区域。开机后正常系统直接起不来,fastboot也进不去,只能尝试救砖。当时手上只有一张朋友之前用读分区工具导出的PGPT备份,没有官方的scatter。就是在那种局面下,我才第一次意识到:PGPT备份远比想象中有用,它记录了整块存储的分区边界,是救砖的“地图”,有了它,重新生成scatter根本不难。

如果你只是正常用手机,PGPT一辈子都用不上;可一旦走到“需要线刷但找不到配套scatter”这一步,它就是你翻盘的关键。所以我现在的习惯是:任何MTK手机到手,只要已经root,先花五分钟做一次PGPT备份,放到电脑和网盘各一份,并记录校验值。这个习惯的回报率极高,不要等砖了才后悔。

1.2 GPT分区表的结构:LBA0到LBA33都在存什么

PGPT在MTK圈子里通常被称为“保护性GPT”或“GPT分区表备份”,本质上它就是存储设备最前面那一段用来描述分区布局的原始数据。现代GPT结构大致长这样:

  • LBA0:保护性MBR,512字节,防止老式磁盘工具不识别GPT盘。
  • LBA1:GPT头,512字节,里面记录“EFI PART”签名、分区表起始位置、分区项数量和大小。
  • LBA2到LBA33:主分区表项,标准128个分区项,每项128字节,一共32个LBA。
  • LBA34及以后:真正的用户分区起点。

MTK平台通常会把前面这34个LBA看成一块特殊的“pgpt区域”,所以你在手机的/dev/block/by-name/下能看到名为 pgpt 的分区。很多教程里说的“备份PGPT”,实际操作就是把这个区域的二进制镜像完整抠出来。

1.3 MTK的scatter文件为什么必须和PGPT一一对应

SP Flash Tool刷机时并不认识分区名,它只认scatter文件里写的linear_start_addr(线性起始地址)和partition_size(分区大小)。你告诉它某个分区在0x440000,它就把数据往0x440000写。如果地址和手机实际的GPT分区布局对不上,后果就是数据写到别人的地盘:把boot写到system区域、把logo写到recovery区域,轻则功能异常,重则直接变成砖。

官方线刷包里的scatter只适用于原厂分区布局。如果手机被刷过第三方分区方案、扩容过userdata、或者官方本身有版本差异,旧scatter就可能与实际布局错位。而基于本机PGPT生成的scatter,是从你设备上当前的真实分区表解析出来的,天然和这台手机绑定,不会再出现“地图和路对不上”的问题。

2. 数据从哪来:导出手机PGPT的几种可靠办法

2.1 条件准备:一台能进系统的root手机加adb

动手之前先确认两件事:手机能正常进入系统,并且已经获得root权限。PGPT备份本质上要直接读取物理块设备,没有root是做不到的。电脑上装好adb工具,手机连接电脑后在开发者选项里打开USB调试。这里我默认你已经会基本adb操作,如果还没搞定adb环境,可以先花几分钟配一下,不影响后面的流程。

另外要确认平台是eMMC还是UFS。绝大多数中低端MTK手机用的是eMMC,块设备路径是/dev/block/mmcblk0;部分新机型用UFS,设备名可能是/dev/block/sda或带lun的路径。如果想偷懒,优先查/dev/block/by-name/目录,那里有直接映射到pgpt分区的软链接,路径差异会小很多。

2.2 用dd命令从eMMC导出PGPT

在root shell里直接用dd命令把物理块设备最前面的34个LBA抠出来,命令很简单:

adb shell su -c "dd if=/dev/block/mmcblk0 of=/sdcard/pgpt.bin bs=512 count=34" adb pull /sdcard/pgpt.bin ./

为什么是count=34?因为LBA0到LBA33正好是前面说的保护性MBR、GPT头和主分区表项。如果你怕平台差异导致分区表项更多(有些平台会写到256项),可以保守一点直接count=64,多读出来的数据不影响解析,只会让备份文件大那么一丁点。

还有一种更精确的方式:直接备份by-name/pgpt分区。

adb shell su -c "ls -l /dev/block/by-name/ | grep pgpt" adb shell su -c "dd if=/dev/block/by-name/pgpt of=/sdcard/pgpt_by_name.bin"

重定向到/sdcard再pull,是因为部分手机的su shell里对/data/local/tmp之外路径的写权限略麻烦,放sdcard最稳。备份完成后建议执行一次sha256sum pgpt.bin记录哈希,今后想确认文件是否损坏就能直接对比。

2.3 已有全量备份时,怎么把PGPT单独抠出来

如果你没有提前备份,但手里恰好有一份之前用SP Flash Tool Readback读出来的全盘镜像(或者从其它渠道拿到的整机备份),也可以把PGPT部分单独抠出来。标准的PGPT区就是文件最前面的0x4400字节(34个LBA×512字节),用Python三行就能搞定:

with open("full_dump.bin", "rb") as f: pgpt = f.read(0x4400) with open("pgpt_from_full.bin", "wb") as f: f.write(pgpt)

多读一点也无所谓,比如直接读0x8000字节,后面多出来的数据解析时会自动忽略。

这里有个很容易忽略的坑:如果全盘镜像是用bs=4096的方式读出来的,那LBA大小就不是512而是4096,前面的0x4400偏移就会错位。判断方法很简单,用十六进制编辑器打开镜像,看偏移0x200(512字节)处是不是“EFI PART”字符串。如果是,说明LBA是512;如果不是,检查偏移0x1000(4096字节)处。找到签名位置后,偏移基准自然就清楚了。

3. 解析与生成:Python脚本完整拆解

3.1 先读懂新版MTK scatter文件的字段规范

在写脚本之前,先认识一下MTK scatter长什么样。现在新平台的scatter文件是类似这样的文本结构:

- general: platform: MT6785 project: my_device storage: EMMC boot_mode: download ram_pt: 0 - partition_index: SYS0 partition_name: preloader file_name: preloader_my_device.bin is_download: true type: SV5_BL_BIN linear_start_addr: 0x0 partition_size: 0x100000 region: EMMC_BOOT_1 storage: HW_STORAGE_EMMC boundary_check: true is_reserved: false operation_type: UPDATE reserved: 0

对GPT解析来说,最关键的两个字段是linear_start_addr(分区起始字节地址)和partition_size(分区大小)。其它字段更多是给SP Flash Tool做人机交互和校验用的。理解了这一点,就能明白为什么只靠PGPT也能生成一份能用的scatter。

3.2 脚本整体设计和依赖选择

脚本只依赖Python标准库,struct负责解包二进制,sys负责参数读取,不需要安装任何第三方包。整体流程是:打开PGPT备份文件,定位并解析GPT头,得到分区表起始位置、分区项数量和每项大小,然后遍历分区项,提取分区名、起始LBA、结束LBA,最后换算成字节地址并套用scatter模板写出配置。

为什么不用别人写好的现成工具?一是很多工具只支持固定分区数量,遇到特殊平台就翻车;二是自己写的脚本可以随时加过滤条件,比如想只导出super、userdata等几个大分区,改一个参数就行;三是整个过程可控,出了问题你知道每一行数据是怎么来的。

3.3 解析GPT头:校验签名、定位分区表

GPT头在文件偏移512字节处(LBA1)。先读16字节判断签名,正常的前8字节是ASCII的“EFI PART”。接下来几个关键偏移:

  • 偏移72:主分区表起始LBA,8字节小端整数。
  • 偏移80:分区项数量,4字节小端整数。
  • 偏移84:每个分区项大小,4字节小端整数。

标准GPT中分区表起始LBA是2,分区项128个,每项128字节。但MTK不同平台不一定完全按标准来,所以脚本里从头部动态读取,不写死。

3.4 提取分区项:名称、起始LBA、大小一个都不能错

分区表项从起始LBA对应的字节偏移开始。每个分区项128字节,关键字段如下:

  • 偏移0:分区类型GUID,16字节。
  • 偏移16:分区唯一GUID,16字节。
  • 偏移32:起始LBA,8字节小端整数。
  • 偏移40:结束LBA(包含),8字节小端整数。
  • 偏移48:分区属性,8字节小端整数。
  • 偏移56:分区名,72字节UTF-16LE编码。

判断一个分区项是否有效,先看类型GUID是不是全零。全零表示空条目,直接跳过。分区大小计算时,结束LBA是包含的,所以(end_lba - start_lba + 1) * 512才是字节数。名称必须用utf-16-le解码,再用rstrip('\x00')去掉末尾的空字符,不然输出里会带一串乱码。

3.5 拼装scatter:地址计算和模板套用

拿到每个分区的起始LBA后,字节地址就等于起始LBA乘以512,转成十六进制填进linear_start_addr。GPT里的分区名一般是全小写,可以直接作为partition_name。type字段可以简单推断:preloader、boot、recovery、vbmeta这类引导相关分区用SV5_BL_BIN,其它分区用NORMAL。这个推断不是绝对标准,但SP Flash Tool对type的容错还算好,后续真遇到加载问题再手动调整。

还有一个必须提醒的事情:从GPT解析出来的分区列表里通常没有preloader。因为preloader不占用用户区LBA,它放在eMMC的BOOT1分区。所以如果线刷时需要preloader,得单独用dd if=/dev/block/mmcblk0boot0导出,并手动在scatter里补一个region为EMMC_BOOT_1的条目。

3.6 可用的完整代码(照抄就能跑)

把下面这段保存成pgpt2scatter.py,然后按注释里的用法执行即可。

#!/usr/bin/env python3 # -*- coding: utf-8 -*- import struct import sys GPT_SIGN = b'EFI PART' LBA_SIZE = 512 def parse_gpt_header(data): header = data[LBA_SIZE:LBA_SIZE * 2] if header[0:8] != GPT_SIGN: raise ValueError('Invalid GPT signature, not a PGPT backup') part_lba = struct.unpack_from('<Q', header, 72)[0] part_count = struct.unpack_from('<I', header, 80)[0] part_size = struct.unpack_from('<I', header, 84)[0] return part_lba, part_count, part_size def parse_partitions(data, part_lba, part_count, part_size): partitions = [] for i in range(part_count): off = part_lba * LBA_SIZE + i * part_size entry = data[off:off + part_size] if len(entry) < part_size: break type_guid = entry[0:16] if not any(type_guid): continue start_lba = struct.unpack_from('<Q', entry, 32)[0] end_lba = struct.unpack_from('<Q', entry, 40)[0] raw_name = entry[56:128].decode('utf-16-le', errors='replace') raw_name = raw_name.rstrip('\x00').strip() partitions.append({ 'name': raw_name, 'start_lba': start_lba, 'end_lba': end_lba, 'size': (end_lba - start_lba + 1) * LBA_SIZE, 'addr': start_lba * LBA_SIZE, }) return partitions def infer_part_type(name): name_lower = name.lower() if name_lower in ('preloader', 'boot', 'recovery', 'vendor_boot', 'vbmeta', 'vbmeta_system', 'vbmeta_vendor'): return 'SV5_BL_BIN' return 'NORMAL' def gen_mtk_scatter(partitions, platform='MT6785', project='pgpt_backup', storage='EMMC'): lines = ['- general:', f' platform: {platform}', f' project: {project}', f' storage: {storage}', ' boot_mode: download', ' ram_pt: 0', ''] for idx, p in enumerate(partitions): lines.append(f'- partition_index: SYS{idx}') lines.append(f' partition_name: {p["name"]}') lines.append(f' file_name: {p["name"]}.img') lines.append(f' is_download: true') lines.append(f' type: {infer_part_type(p["name"])}') lines.append(f' linear_start_addr: 0x{p["addr"]:X}') lines.append(f' partition_size: 0x{p["size"]:X}') lines.append(' region: EMMC_USER') lines.append(' storage: HW_STORAGE_EMMC') lines.append(' boundary_check: true') lines.append(' is_reserved: false') lines.append(' operation_type: UPDATE') lines.append(' reserved: 0') lines.append('') return '\n'.join(lines) if __name__ == '__main__': if len(sys.argv) < 2: print('用法: python pgpt2scatter.py 你的pgpt备份.bin [平台名] [工程名]') sys.exit(1) pgpt_path = sys.argv[1] platform = sys.argv[2] if len(sys.argv) > 2 else 'MT6785' project = sys.argv[3] if len(sys.argv) > 3 else 'pgpt_backup' with open(pgpt_path, 'rb') as f: data = f.read() part_lba, part_count, part_size = parse_gpt_header(data) partitions = parse_partitions(data, part_lba, part_count, part_size) scatter = gen_mtk_scatter(partitions, platform=platform, project=project) out_path = pgpt_path + '.scatter.txt' with open(out_path, 'w', encoding='utf-8') as f: f.write(scatter) print(f'OK, 共解析 {len(partitions)} 个分区,已输出: {out_path}')

用法示例:

python pgpt2scatter.py pgpt.bin MT6765 my_project

脚本会在当前目录生成pgpt.bin.scatter.txt。用SP Flash Tool新建下载任务时选中这个txt即可。

补充一个Windows下容易遇到的问题:如果控制台打印中文乱码,先执行chcp 65001把代码页切到UTF-8,或者在运行时把系统区域设置里的“Beta版使用Unicode UTF-8提供全球语言支持”打开。另外,生成的scatter文件不要拿记事本另存为带BOM的UTF-8,SP Flash Tool对BOM比较敏感,推荐用VSCode或Notepad++保持无BOM的UTF-8。

4. 实战验证与踩坑记录:生成的scatter到底能不能用

4.1 用SP Flash Tool加载前,先做一遍反向校验

生成scatter之后,别急着开刷,先做一次反向校验。最简单的办法是用文本编辑器打开生成的scatter,逐项检查linear_start_addr是不是递增的,partition_size是不是和手机里的大分区实际情况吻合。一般来说,第一个用户分区(通常是proinfo或bmtpool)起始地址不会从0开始,而是某个有一定偏移的位置,这说明MTK把前面一段留给了PGPT和bootloader。

如果想更严谨,可以写一个几行的小脚本,把scatter里的地址除以512还原成LBA,再和PGPT里的起始LBA对比。在手机还能临时进系统的情况下,也可以用cat /proc/partitions看每个分区的块数量,和生成的partition_size做比对。这一步能过滤掉绝大多数低级错误。

4.2 刷机过程中最容易翻车的几个点

问题现象根因解决办法
SP Flash Tool不加载某个分区type字段不符根据分区名手动改type,引导类换SV5_BL_BIN,其它用NORMAL
刷完其它分区正常,但开不了机preloader缺失或region填错从mmcblk0boot0导出preloader,手动补条目并把region设为EMMC_BOOT_1
地址错位、数据刷到相邻分区计算地址时没乘512,或结束LBA处理错误用脚本重新生成,并通过4.1的反向校验
勾选了system但刷了没反应Android 10+动态分区,system被合并进super直接刷super分区,不要单独勾选system

这些坑我基本都踩过一遍,尤其是动态分区那个,最容易让新手困惑。很多MTK新机的GPT里已经没有独立的system、vendor、product分区条目,而是只有一个super。你解析出来的分区表里如果看到super,别觉得是解析错了,这就是新版Android的常见布局。

4.3 不同平台和安卓版本下的差异:eMMC、UFS、动态分区

MTK这两年平台差异主要集中在存储介质和分区策略两方面。存储方面,eMMC的逻辑块大小一般是512字节,UFS虽然物理扇区通常更大,但逻辑块地址仍按512或4096字节暴露,具体以哪个为准,看GPT头在镜像里出现在偏移0x200还是0x1000就知道。Android版本方面,Android 10开始默认动态分区,GPT里的分区数量和名称改了,但PGPT的解析逻辑完全不受影响,因为它读的是二进制结构,不是分区语义。

最后说一句偏经验的话:不同芯片平台的scatter格式大同小异,但SP Flash Tool版本对老格式和新格式的兼容性不一样。MT6765以前的机器,有人还在用老版本工具,生成的字段太多反而可能报错。遇到这种老机器,可以在生成时只保留general和partition_index、partition_name、linear_start_addr、partition_size这几个字段,实测老工具也能认。

我个人在实际操作中的体会是:备份PGPT这件事,成本极低但收益极高。MTK手机到手,第一件事就建议先root、先备份整块mmcblk0最前面一段,然后把备份文件丢到电脑和网盘各存一份,顺手算一个SHA256。等到哪天真需要线刷却没有scatter时,你会感谢当初花五分钟做的这个备份。这套脚本我自己在MT6765、MT6785、MT6833三个平台上验证过,生成的scatter配合SP Flash Tool都能正常识别。如果你在某个机型上发现解析出来的分区信息有异常,优先回头检查PGPT备份文件本身是否完整,再对照偏移表确认签名和分区项位置,通常问题都出在数据源,而不是脚本逻辑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询