ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MTK刷机救砖必备:Python备份PGPT分区表与生成scatter全攻略

MTK刷机救砖必备:Python备份PGPT分区表与生成scatter全攻略 干了这么多年手机维修和刷机方向的技术支持我越来越确定一件事MTK机型在刷机、救砖、数据恢复这些场景里最值得提前备份的不是某个App的数据而是那份藏在存储最前端的PGPT分区表。只要这份“分区地图”还在手机就算被刷得完全开不了机也有很大机会把砖救回来而一旦PGPT损坏后续所有操作都会变得极其被动。这篇教程就是把这件事彻底讲透从理解PGPT是什么到用Python解析scatter刷机配置、自动校验分区表CRC再到从备份的PGPT反推出可用的线刷引导文件包含scatter配置和preloader引导镜像的补全思路。全程按保姆级标准写只要有基础的电脑操作能力跟着一步步做就能给自己手里的MTK机器建立一套完整的分区备份与引导恢复方案。这篇内容适合手机维修从业者、玩机进阶用户以及经常和数据恢复打交道的技术朋友。1. 项目概述一张分区表凭什么值得单独备份1.1 PGPT在MTK手机里的真实身份MTK平台的智能手机无论是eMMC存储还是UFS存储在出厂时都会被划分为几十个区域比如preloader、proinfo、nvram、boot、system、vendor等等。这些区域的起始位置、大小、名称全部记录在存储前端的GUID分区表里。GUID分区表有两份位于存储最前端的叫PGPT也就是Primary GPT位于存储末尾的叫SGPT也就是Secondary GPT。用一张图来理解整块存储就像一栋大楼PGPT是贴在门口的总平面图SGPT是贴在楼后门的总平面图。刷机工具要往“房间”里写数据得先看总平面图才知道房间在哪、面积多大。一旦这张总平面图被破坏刷机工具就不知道该往哪里写系统自然起不来。MTK平台的特殊之处在于它的线刷工具SP Flash Tool依赖两类关键信息一是存储在boot区域里的preloader引导加载器二是描述所有分区布局的scatter文件。很多维修师傅手里有原厂固件但固件包里没带scatter或者带的scatter版本对不上这时候手机插上电脑工具根本不知道如何下手。如果提前备份了PGPT我们就可以用Python脚本从这份原始二进制里把所有分区的位置和大小重新算出来生成一份新的scatter文件等于把“总平面图”重新画了出来。1.2 Python工具在这件事里负责什么市面上也有现成的分区查看工具但大多只能看不能改或者需要特定版本的图形界面遇到异常CRC、分区项损坏、格式不兼容时几乎没有办法。Python处理这种二进制结构有天然优势标准库里就有struct和zlib解析固定字节长度的GPT头、计算CRC32都不需要第三方库脚本写完在任何一台电脑上都能跑。这套方案里Python主要负责三件事。第一解析原厂scatter文件把分区名称、线性地址、分区大小提取成结构化数据为后续读回备份提供精确的地址参数。第二解析读回的分区表镜像校验GPT头和分区项数组的CRC确认备份数据本身没有损坏。第三根据解析结果生成新的scatter文件并扫描定位preloader等关键引导镜像完成线刷引导文件的组装。我在这套流程里用的是Python 3.8以上的版本全程没有依赖Linux环境Windows系统直接跑对维修店里的电脑非常友好。接下来从环境准备开始逐步把整套操作讲清楚。2. 环境准备Python环境与必要依赖2.1 安装Python运行环境如果你电脑里已经装了Python打开命令行输入python --version能输出版本号就可以跳过这一小节。没装的话去Python官网下载Windows安装包安装时务必勾选“Add Python to PATH”这一步很关键不勾选后面在命令行里敲python会提示找不到命令。安装完成后按WinR输入cmd回车在弹出的命令行窗口里验证一下python --version pip --version两个命令都能正常输出说明环境OK。Mac和Linux系统的用户一般自带Python 3但保险起见也建议先验证一下。2.2 准备基线文件与工作目录在开始写脚本之前需要先建立一个清晰的工作目录我的习惯是建一个专门存放工具和备份的文件夹比如D:\mtk_work下面再分三个子目录scatter_from_firmware存放固件里提取的原厂scatter文件gpt_backup存放读回的分区表原始镜像output存放脚本生成的中间结果和最终的线刷引导文件这套流程用到的Python库非常少只有os、re、struct、zlib这几个标准库所以不需要额外安装任何第三方包。如果你的电脑之前装过Anaconda之类的科学计算环境也没有冲突直接使用系统自带的Python解释器运行即可。这里还要说明一点脚本需要读入的“基线文件”有两个来源。一个是原厂固件里的scatter文件通常在刷机包根目录文件名类似MT6765_Android_scatter.txt另一个是从手机读回的GPT原始镜像本文后续用gpt_backup.img表示。如果你手头已经有这些文件现在就可以把它们复制到对应目录里。如果还没有先跟着读完第4章用我给的地址参数完成一次读回操作就能拿到原始镜像。3. 读懂scatter文件Python解析MTK刷机配置3.1 scatter文件的信息结构MTK的scatter文件本质上是一个有固定格式的纯文本文件里面以- partition_index: SYS0这样的标记开始紧接着列出该分区的一系列属性。不同固件版本字段会有差异但以下几个字段是通用的字段含义示例partition_index分区序号SYS0partition_name分区名称preloaderfile_name线刷时使用的镜像文件名preloader_k65v1.binlinear_start_addr线性起始地址0x0physical_start_addr物理起始地址0x0partition_size分区大小0x400000region分区所在存储区域EMMC_BOOT_1storage存储类型HW_STORAGE_EMMCtype镜像类型svf这里特别强调一下region字段。MTK的eMMC设备除了用户可见的user区还有两个boot区域编号分别是EMMC_BOOT_1和EMMC_BOOT_2。preloader这个分区不在user区里而是放在EMMC_BOOT_1。很多新手第一次看到scatter文件时发现preloader的起始地址是0x0就在Read Back时去读user区的0x0结果读回来一堆0这就是没理解region的含义。从GPT的角度看GPT记录的是user区域内的分区布局而EMMC_BOOT_1是独立的另一次存储空间不受PGPT管理。所以在做解析时一定要把scatter里的region和linear_start_addr一起读出来不能只看地址。3.2 用Python脚本批量提取分区地址直接写一个解析scatter文件的脚本把需要的信息提取成结构化数据。这个脚本同时承担一个很重要的任务为后续读回GPT提供“参考地址”。新建parse_scatter.py内容如下import re import sys def parse_scatter(path): blocks [] current {} with open(path, r, encodingutf-8, errorsreplace) as f: for line in f: line line.strip() if not line or line.startswith(#) or line.startswith(//): continue m re.match(r- (\w):\s*(.*), line) if m: key, value m.group(1), m.group(2).strip() if key partition_index: if current: blocks.append(current) current {partition_index: value} else: current[key] value if current: blocks.append(current) return blocks if __name__ __main__: scatter_blocks parse_scatter(sys.argv[1]) for block in scatter_blocks: name block.get(partition_name, ) if name pgpt or name sGPT or name boot: print({:16} {:12} {:12} {}.format( block.get(partition_name), block.get(linear_start_addr), block.get(partition_size), block.get(region) ))运行方式python parse_scatter.py MT6765_Android_scatter.txt这段代码有一个很实用的地方它会特别筛选出pgpt、sGPT、boot这几个关键分区。原厂scatter里如果带了pgpt分区那么它记录的位置就是我们要备份的PGPT所在区域。不过有些固件的scatter并不单独列出pgpt这时候就需要靠GPT头签名去定位了。解析输出示例preloader 0x0 0x400000 EMMC_BOOT_1 pgpt 0x0 0x800000 EMMC_USER boot 0x100000 0x2000000 EMMC_USER看到pgpt分区的起始地址是0x0大小是0x800000意思是在user区域的开头8MB空间里放着分区表和相关数据。这个8MB并不全是分区表通常只用了很靠前的一小部分但读回时把它整个读出来最稳妥。4. PGPT备份实操从读回镜像到校验通过4.1 一线师傅最常用的读回方式备份PGPT最可靠的方式是使用SP Flash Tool的Read Back功能。操作流程是这样的手机先关机打开SP Flash Tool切到Read Back标签页点击Add按钮添加一条读取记录填写起始地址和长度选择保存文件路径然后点击Read Back此时再将已经关机的手机通过数据线连接到电脑工具检测到BROM设备后就会自动开始读取。地址和长度怎么填根据上一节解析出的scatter文件如果固件自带pgpt分区直接使用它的起始地址和分区大小。如果scatter里没有pgpt那就读user区开头的1MB也就是起始地址0x0、长度0x100000。这样能把保护性MBR、GPT头、128个分区项全部覆盖到正常情况下GPT头在1号扇区即偏移0x200处分区项数组从2号扇区开始到33号扇区结束1MB的空间绰绰有余。读取过程中千万不要碰数据线也不要做任何让手机通电断电的操作。读回操作本身是纯读取不会往手机里写入任何数据所以风险很低真正的危险发生在后面写回时。这个备份文件就是我们的“救命稻草”读回来之后我会建议立刻复制一份放到其他磁盘或者U盘里。4.2 用Python解析GPT并校验CRC读回的文件是原始二进制镜像直接用十六进制编辑器看能认出“EFI PART”字样但信息太不直观。我写了一个GPT解析脚本能够读出所有分区项并对完整性做双重CRC校验。新建parse_gpt.py内容如下import struct import sys import zlib GPT_SIGNATURE bEFI PART SECTOR_SIZE 512 def read_gpt_header(img_path, lba1, sector512): with open(img_path, rb) as f: f.seek(lba * sector) data f.read(sector) if data[:8] ! GPT_SIGNATURE: raise ValueError(GPT signature not found, file may be corrupted or wrong region) return data def verify_header_crc(header_block): header_size struct.unpack_from(I, header_block, 12)[0] stored_crc struct.unpack_from(I, header_block, 16)[0] tmp bytearray(header_block[:header_size]) struct.pack_into(I, tmp, 16, 0) calc_crc zlib.crc32(bytes(tmp)) 0xffffffff return stored_crc calc_crc, stored_crc, calc_crc def parse_gpt_entries(img_path, header_block): partition_entry_lba struct.unpack_from(Q, header_block, 72)[0] num_entries struct.unpack_from(I, header_block, 80)[0] entry_size struct.unpack_from(I, header_block, 84)[0] stored_array_crc struct.unpack_from(I, header_block, 88)[0] with open(img_path, rb) as f: f.seek(partition_entry_lba * SECTOR_SIZE) entries f.read(num_entries * entry_size) calc_array_crc zlib.crc32(entries) 0xffffffff print(Partition array CRC: stored{:#010x}, calc{:#010x}, match{}.format( stored_array_crc, calc_array_crc, stored_array_crc calc_array_crc)) partitions [] for i in range(num_entries): entry entries[i * entry_size:(i 1) * entry_size] if not entry or entry b\x00 * entry_size: continue first_lba, last_lba, attrs struct.unpack_from(QQQ, entry, 32) name entry[56:128].decode(utf-16-le).rstrip(\x00) partitions.append({ index: i 1, name: name, first_lba: first_lba, last_lba: last_lba, size: (last_lba - first_lba 1) * SECTOR_SIZE, attrs: attrs, }) return partitions if __name__ __main__: img_path sys.argv[1] header read_gpt_header(img_path) ok, stored, calc verify_header_crc(header) print(GPT header CRC: stored{:#010x}, calc{:#010x}, match{}.format(stored, calc, ok)) pts parse_gpt_entries(img_path, header) print(total valid partitions:, len(pts)) for p in pts[:20]: print({:20} start{:#012x} size{:#012x}.format( p[name], p[first_lba] * SECTOR_SIZE, p[size]))运行python parse_gpt.py gpt_backup.img这里我解释一下两个CRC校验的意义。GPT头里保存了两个CRC32第一个是GPT头自身前92字节的校验值第二个是分区项数组的校验值。如果文件在读取或保存过程中出现了字节错位、截断、中间插入空字符等情况CRC一定会对不上。只要两个CRC校验都通过就说明这备份下来的PGPT是完整可靠的。脚本运行结果的matchTrue是理想状态。如果提示GPT signature not found要么是读回地址填错了读到的不是分区表区域要么是文件被文本编辑器动过。记住备份出来的.bin文件千万不要用记事本打开再保存否则文件编码和换行符都会被改坏。4.3 备份文件的最佳存放格式读回成功并校验通过后我建议保留三份文件第一份是原始镜像gpt_backup.img这必须保持原样不改任何字节。第二份是Python脚本生成的解析报告可以把上面脚本的输出重定向保存为gpt_partition_report.txt方便以后快速查看分区布局。第三份是提取出来的PGPT头文件可以用脚本把偏移0x200到0x4200之间的字节单独导出为pgpt_header_only.bin后续做分区表修复时不用每次翻大文件。用Python导出头部区域的代码非常简单with open(gpt_backup.img, rb) as f: data f.read(0x4200) with open(pgpt_header_only.bin, wb) as f: f.write(data)0x4200这个值是怎么来的GPT头占1个扇区即512字节从1号扇区到33号扇区是分区项数组共32个扇区16384字节。加上第0扇区的保护性MBR总共是2号扇区之前的内容即从偏移0x0到0x4200。把这个范围单独导出来就是完整的分区表定义。5. 线刷引导文件生成从PGPT反推可用scatter5.1 什么是“线刷引导文件”为什么常缺线刷引导文件是我对“刷机时SP Flash Tool必须的那一组基础文件”的统称主要包括三个部分scatter配置、preloader引导镜像、以及DA下载代理。其中DA通常集成在刷机工具的版本里不需要我们操心真正容易缺失的是前两个。现实中我经常遇到的情况是客户拿来一台MTK机器手机能进BROM模式但原厂固件包是网上找的里面只有一个MT6765_Android_scatter.txt引用的preloader文件名在实际包中根本不存在或者固件包里连scatter都没有只有一个完整的分区备份。这时候只要手里有PGPT备份我们就能重新生成一份匹配的scatter并利用脚本从全量备份中定位preloader镜像把整套引导文件补全。PGPT里记录着每个分区的first_lba和last_lba每个LBA按512字节计算那么分区的线性起始地址就是first_lba * 512分区大小就是(last_lba - first_lba 1) * 512。GPT里分区项本身就是一份完整的“分区地图”把它转换成scatter格式就是线刷引导文件生成最核心的一步。5.2 Python完成scatter重建脚本设计与输出新建gpt_to_scatter.py这个脚本读取上一步解析出的分区表按MTK scatter的通用格式输出文件。核心逻辑分三块判断分区属于哪个region、计算地址和大小、拼接文本。import struct import zlib import sys SECTOR_SIZE 512 def parse_gpt_entries(img_path): with open(img_path, rb) as f: f.seek(SECTOR_SIZE) header f.read(SECTOR_SIZE) if header[:8] ! bEFI PART: raise ValueError(bad GPT signature) entry_lba struct.unpack_from(Q, header, 72)[0] num_entries struct.unpack_from(I, header, 80)[0] entry_size struct.unpack_from(I, header, 84)[0] with open(img_path, rb) as f: f.seek(entry_lba * SECTOR_SIZE) entries f.read(num_entries * entry_size) partitions [] for i in range(num_entries): entry entries[i * entry_size:(i 1) * entry_size] if not entry or entry b\x00 * entry_size: continue first_lba, last_lba, attrs struct.unpack_from(QQQ, entry, 32) name entry[56:128].decode(utf-16-le).rstrip(\x00) partitions.append({ name: name, start: first_lba * SECTOR_SIZE, size: (last_lba - first_lba 1) * SECTOR_SIZE, }) return partitions def generate_scatter(partitions, out_path): lines [] lines.append(- general:) lines.append( load_type: SV5) lines.append( scatter_file_ver: 5) lines.append( is_auto_download: 0) lines.append() for idx, p in enumerate(partitions): name p[name].lower() if name preloader: region EMMC_BOOT_1 file_name preloader_generated.bin else: region EMMC_USER file_name {}.img.format(p[name]) lines.append(- partition_index: SYS{}.format(idx)) lines.append( partition_name: {}.format(p[name])) lines.append( file_name: {}.format(file_name)) lines.append( type: svf) lines.append( linear_start_addr: {:#x}.format(p[start])) lines.append( physical_start_addr: {:#x}.format(p[start])) lines.append( partition_size: {:#x}.format(p[size])) lines.append( region: {}.format(region)) lines.append( storage: HW_STORAGE_EMMC) lines.append( boundary_check: 0) lines.append( is_upgradable: 1) lines.append( operation_type: UPDATE) lines.append( is_download: 1) lines.append() with open(out_path, w, encodingutf-8) as f: f.write(\n.join(lines)) print(scatter written to, out_path) if __name__ __main__: pts parse_gpt_entries(sys.argv[1]) generate_scatter(pts, sys.argv[2])运行python gpt_to_scatter.py gpt_backup.img generated_scatter.txt生成出来的scatter里preloader分区会被自动归到EMMC_BOOT_1并通过file_name: preloader_generated.bin告诉刷机工具去找哪个镜像文件。其他所有GPT里记录的分区都会逐一列出起始地址、分区大小完全按照备份里的原始值来。用这个脚本之前必须确认一件事读回的镜像确实来自设备存储的user区域起始处而不是某个误操作读出来的其他区域。验证方法很简单生成的scatter里第一个用户分区通常是proinfo或者nvram起始地址一般在0x400000到0x1000000之间。如果看到第一个分区就是system或者地址特别大那就要检查镜像是不是被截断了。5.3 从全量备份中提取preloader并补全引导包scatter文件搞定了接下来要解决preloader镜像。preloader存放在EMMC_BOOT_1区域不归PGPT管所以刚才解析PGPT时根本看不到它。要得到这份镜像最直接的办法是在SP Flash Tool里再执行一次Read Back选择EMMC_BOOT分区起始地址0x0长度按scatter里preloader记录的大小来通常是0x400000。如果手头只有全量备份比如之前用工具对整个eMMC做过完整备份同样可以用Python来定位preloader。MTK的preloader镜像头部一般有明显的魔数特征常见的是从某个偏移位置开始出现连续多次重复的MMM字节模式。扫描脚本思路如下def find_preloader(data, start0, endNone): end end or len(data) idx data.find(bMMM, start, end) candidates [] while idx ! -1: if data[idx:idx 0x20].count(bMMM) 2: candidates.append(idx) idx data.find(bMMM, idx 1, end) return candidates扫描到候选位置之后先检查文件大小是否和scatter里preloader分区大小一致再查看文件尾部是否有合法的镜像结束标记。确认无误后把这段数据导出为preloader_generated.bin放到scatter同一目录下线刷引导文件包就算基本齐了。这里要特别提醒不同MTK平台的preloader头部结构有差异靠魔数扫描只是一种辅助定位手段不能替代对固件包结构和平台规范的确认。条件允许时优先从同版本原厂固件包里提取preloader生成文件仅作为无包可用的应急方案。5.4 生成后的实机验证流程生成的scatter和preloader最终能不能用必须实机验证一次。打开SP Flash Tool切到Download标签页点击Scatter-loading导入generated_scatter.txt。工具会自动列出所有分区确认preloader那一行的文件路径指向我们提取出来的preloader_generated.bin再检查boot、system等分区是否处于可下载状态。验证阶段建议只勾选preloader和boot两个分区。把手机进入BROM模式后点击Download等工具走完“Download DA”和“Download preloader”两步看到绿色对勾就算通过。这一步能跑通说明生成的scatter地址正确、preloader镜像版本匹配整套引导文件是有效的。如果卡在“Download DA”之后报错先别急着怀疑脚本。检查一下SP Flash Tool版本和DA版本是否匹配换一个版本的刷机工具再试。MTK的DA兼容性是个老话题同一个平台在不同工具版本下的表现经常不一样多试几个版本是正常的。6. 全程踩坑记录备份过程中的高危雷区6.1 最容易翻车的五个操作点第一个坑是读回地址填错最常见的是把EMMC_BOOT_1当成user区域或者把user区域的0x0读成了boot区域的0x0。解决办法是在scatter解析脚本里同时打印region和地址读回时对着填。第二个坑是用文本编辑器动过备份文件。很多师傅习惯用记事本打开bin文件“看一眼”再顺手CtrlS保存文件就在不知不觉中被破坏。正确做法是永远只保留原始bin文件需要查看时拷贝副本再看。第三个坑是CRC校验不通过还继续使用备份。如果GPT头的CRC显示不匹配说明这份备份是不可信的用它生成scatter只会得到错误结果必须重新读回或者找回其他版本的备份。第四个坑是UFS机型的扇区大小问题。部分UFS设备的逻辑扇区是4096字节而不是512字节如果还用512字节去解析所有地址都会错位。脚本里我把SECTOR_SIZE定义成常量遇到UFS 4Kn设备时改为4096再重跑即可。第五个坑是不同固件版本的GPT分区项大小不是128字节。虽然绝大多数设备遵循128字节分区项规范但个别特殊设备可能改过解析时如果发现分区名乱码检查一下GPT头里记录的分区项大小字段把脚本里的entry_size替换成实际值。6.2 某些MTK机型不按套路出牌MTK平台虽然整体方案统一但不同厂商在量产时会做不同程度的定制。有的厂商把preloader放到user区域而不是EMMC_BOOT_1它的scatter里preloader的region会显示为EMMC_USER这时候再用region字段做判断就可能出错。代码里我对region的判断逻辑是一个基础版本真遇到非标机型建议同时参考原厂scatter的region设置以原厂文件为准。还有一种情况是PGPT与SGPT不一致。正常设备两份分区表内容应该相同但有些设备在系统升级后只更新了PGPTSGPT保留着旧版数据。解析时如果用备份GPT去恢复可能恢复出旧版分区布局。我习惯在备份时同时读回user区域最末尾的1MB数据里面包含了SGPT头解析之后和PGPT对比一下能发现很多隐藏问题。针对这类非标情况最有效的防御手段仍然是刷机前完整备份原厂scatter和PGPT并且把备份文件按机型、固件版本、日期命名。一个规范的命名习惯能在关键时刻省下大量排查时间。6.3 常用问题排查速查表现象可能原因处理方法脚本报“GPT signature not found”读回地址错误或文件被改动重新确认地址重新读回不要用编辑器保存binGPT头CRC匹配但分区项CRC不匹配分区项数组读回不完整检查读回长度是否覆盖到33号扇区以上分区名出现中文乱码或空字符串entry_size与设备实际不符读取GPT头中的entry_size字段将其传给解析函数生成scatter后工具提示preloader找不到preloader未提取或文件名不匹配确认preloader_generated.bin位于scatter同目录Download时卡在DA阶段工具版本与DA不兼容换其他版本SP Flash Tool关闭杀软再试读回速度异常慢数据线质量问题或USB端口供电不足使用原装数据线换主机后面板USB口读回的GPT头里的“EFI PART”错位一个字节读回区域选错读到的是其他数据用解析后的scatter核对pgpt分区位置这张表看起来简单但每一条都是实际操作中真实踩过的坑。尤其是“分区名乱码”这个问题很多师傅以为是手机型号太老其实是解析脚本没有按实际entry_size读取把相邻的分区项错位拼接了。实操心得与个人建议整套流程跑了无数遍之后我的体会是MTK刷机这件事真正值钱的不是刷机工具本身而是对分区布局的“确定感”。有了可靠的PGPT备份和一份能用的scatter遇到任何变砖异常都不会慌因为你知道自己随时可以把手机恢复到正确的分区结构上。最后再分享一个小习惯我现在每次给MTK机器做任何写入类操作之前都会先顺手读回一份PGPT备份文件名直接写成“机型_IMEI后四位_日期_gpt.img”存在单独的备份盘里。这个习惯帮我解决过至少四位客户的救砖需求也让我在接到陌生机型时不用反复试探就能快速确定该从哪个地址开始操作。希望这篇保姆级教程也能把这套方法完整传递给你让分区备份从“偶尔想起”变成“固定动作”。
返回列表