diff --git a/README.md b/README.md index 476bbd4..8ffafdb 100644 --- a/README.md +++ b/README.md @@ -101,3 +101,6 @@ https://android.googlesource.com/platform/system/core/+/master/mkbootimg/ Android version list https://source.android.com/source/build-numbers.html + +kernel info extractor +https://android.googlesource.com/platform/build/+/refs/heads/master/tools/extract_kernel.py diff --git a/bbootimg/build.gradle b/bbootimg/build.gradle index 64574b8..0c1503f 100644 --- a/bbootimg/build.gradle +++ b/bbootimg/build.gradle @@ -47,6 +47,8 @@ dependencies { //compile("org.nd4j:nd4j-api:0.9.1") compile("com.google.guava:guava:18.0") compile("org.bouncycastle:bcprov-jdk15on:1.57") + compile("org.apache.commons:commons-exec:1.3") + compile("de.vandermeer:asciitable:0.3.2") } mainClassName = "cfig.RKt" diff --git a/bbootimg/src/main/kotlin/InfoTable.kt b/bbootimg/src/main/kotlin/InfoTable.kt new file mode 100644 index 0000000..13cd268 --- /dev/null +++ b/bbootimg/src/main/kotlin/InfoTable.kt @@ -0,0 +1,8 @@ +package cfig + +import de.vandermeer.asciitable.AsciiTable + +object InfoTable { + val instance = AsciiTable() + val missingParts = mutableListOf() +} \ No newline at end of file diff --git a/bbootimg/src/main/kotlin/Parser.kt b/bbootimg/src/main/kotlin/Parser.kt index c8ba14d..e4e8228 100644 --- a/bbootimg/src/main/kotlin/Parser.kt +++ b/bbootimg/src/main/kotlin/Parser.kt @@ -1,7 +1,9 @@ package cfig import cfig.bootimg.BootImgInfo +import cfig.kernel_util.KernelExtractor import com.fasterxml.jackson.databind.ObjectMapper +import de.vandermeer.asciitable.AsciiTable import org.apache.commons.exec.CommandLine import org.apache.commons.exec.DefaultExecutor import org.junit.Assert.assertTrue @@ -54,14 +56,25 @@ class Parser { return info2 } + fun parseKernelInfo(kernelFile: String) { + val ke = KernelExtractor() + if (ke.envCheck()) { + ke.run(kernelFile, File(".")) + } + } + fun extractBootImg(fileName: String, info2: BootImgInfo) { val param = ParamConfig() + + InfoTable.instance.addRule() if (info2.kernelLength > 0U) { Helper.extractFile(fileName, param.kernel, info2.kernelPosition.toLong(), info2.kernelLength.toInt()) log.info(" kernel dumped to: ${param.kernel}, size=${info2.kernelLength.toInt() / 1024.0 / 1024.0}MB") + InfoTable.instance.addRow("kernel", param.kernel) + parseKernelInfo(param.kernel) } else { throw RuntimeException("bad boot image: no kernel found") } @@ -74,7 +87,11 @@ class Parser { log.info("ramdisk dumped to: ${param.ramdisk}") Helper.unGnuzipFile(param.ramdisk!!, param.ramdisk!!.removeSuffix(".gz")) unpackRamdisk(UnifiedConfig.workDir, param.ramdisk!!.removeSuffix(".gz")) + InfoTable.instance.addRule() + InfoTable.instance.addRow("ramdisk", param.ramdisk!!.removeSuffix(".gz")) + InfoTable.instance.addRow("\\-- extracted ramdisk rootfs", "${UnifiedConfig.workDir}root") } else { + InfoTable.missingParts.add("ramdisk") log.info("no ramdisk found") } @@ -84,7 +101,10 @@ class Parser { info2.secondBootloaderPosition.toLong(), info2.secondBootloaderLength.toInt()) log.info("second bootloader dumped to ${param.second}") + InfoTable.instance.addRule() + InfoTable.instance.addRow("second bootloader", param.second) } else { + InfoTable.missingParts.add("second bootloader") log.info("no second bootloader found") } @@ -93,8 +113,11 @@ class Parser { param.dtbo!!, info2.recoveryDtboPosition.toLong(), info2.recoveryDtboLength.toInt()) - log.info("dtbo dumped to ${param.dtbo}") + log.info("recovery dtbo dumped to ${param.dtbo}") + InfoTable.instance.addRule() + InfoTable.instance.addRow("recovery dtbo", param.dtbo) } else { + InfoTable.missingParts.add("recovery dtbo") if (info2.headerVersion > 0U) { log.info("no recovery dtbo found") } else { @@ -108,7 +131,10 @@ class Parser { info2.dtbPosition.toLong(), info2.dtbLength.toInt()) log.info("dtb dumped to ${param.dtb}") + InfoTable.instance.addRule() + InfoTable.instance.addRow("dtb", param.dtb) } else { + InfoTable.missingParts.add("dtb") if (info2.headerVersion > 1U) { log.info("no dtb found") } else { diff --git a/bbootimg/src/main/kotlin/R.kt b/bbootimg/src/main/kotlin/R.kt index a01b0b1..a696baa 100755 --- a/bbootimg/src/main/kotlin/R.kt +++ b/bbootimg/src/main/kotlin/R.kt @@ -1,6 +1,7 @@ package cfig import cfig.bootimg.BootImgInfo +import de.vandermeer.asciitable.AsciiTable import org.slf4j.LoggerFactory import java.io.File @@ -27,14 +28,27 @@ fun main(args: Array) { if (File(UnifiedConfig.workDir).exists()) File(UnifiedConfig.workDir).deleteRecursively() File(UnifiedConfig.workDir).mkdirs() val info = Parser().parseBootImgHeader(fileName = args[1], avbtool = args[3]) + InfoTable.instance.addRule() + InfoTable.instance.addRow("image info", ParamConfig().cfg) if (info.signatureType == BootImgInfo.VerifyType.AVB) { log.info("continue to analyze vbmeta info in " + args[1]) Avb().parseVbMeta(args[1]) + InfoTable.instance.addRule() + InfoTable.instance.addRow("AVB info", Avb.getJsonFileName(args[1])) if (File("vbmeta.img").exists()) { Avb().parseVbMeta("vbmeta.img") } } Parser().extractBootImg(fileName = args[1], info2 = info) + + InfoTable.instance.addRule() + val tableHeader = AsciiTable().apply { + addRule() + addRow("What", "Where") + addRule() + } + log.info("\n\t\t\tUnpack Summary of ${args[1]}\n{}\n{}", tableHeader.render(), InfoTable.instance.render()) + log.info("Following components are not present: ${InfoTable.missingParts}") } "pack" -> { Packer().pack(mkbootfsBin = args[5]) diff --git a/bbootimg/src/main/kotlin/kernel_util/KernelExtractor.kt b/bbootimg/src/main/kotlin/kernel_util/KernelExtractor.kt new file mode 100644 index 0000000..86fe954 --- /dev/null +++ b/bbootimg/src/main/kotlin/kernel_util/KernelExtractor.kt @@ -0,0 +1,69 @@ +package cfig.kernel_util + +import cfig.InfoTable +import de.vandermeer.asciitable.AsciiTable +import org.apache.commons.exec.CommandLine +import org.apache.commons.exec.DefaultExecutor +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.File + +class KernelExtractor { + val log: Logger = LoggerFactory.getLogger("KernelExtractor") + + fun envCheck(): Boolean { + try { + Runtime.getRuntime().exec("lz4 --version") + log.debug("lz4 available") + } catch (e: Exception) { + log.warn("lz4 unavailable") + return false + } + + try { + Runtime.getRuntime().exec("xz --version") + log.debug("xz available") + } catch (e: Exception) { + log.warn("xz unavailable") + return false + } + + try { + Runtime.getRuntime().exec("gzip -V") + log.debug("gzip available") + } catch (e: Exception) { + log.warn("gzip unavailable") + return false + } + + return true + } + + fun run(fileName: String, workDir: File? = null) { + val baseDir = "build/unzip_boot" + val kernelVersionFile = "$baseDir/kernel_version.txt" + val kernelConfigFile = "$baseDir/kernel_configs.txt" + val cmd = CommandLine.parse("external/extract_kernel.py").let { + it.addArgument("--input") + it.addArgument(fileName) + it.addArgument("--output-configs") + it.addArgument(kernelConfigFile) + it.addArgument("--output-version") + it.addArgument(kernelVersionFile) + } + DefaultExecutor().let { + it.workingDirectory = workDir ?: File("../") + try { + it.execute(cmd) + log.info(cmd.toString()) + val kernelVersion = File(kernelVersionFile).readLines() + log.info("kernel version: " + kernelVersion) + log.info("kernel config dumped to : $kernelConfigFile") + InfoTable.instance.addRow("\\-- version $kernelVersion", kernelVersionFile) + InfoTable.instance.addRow("\\-- config", kernelConfigFile) + } catch (e: org.apache.commons.exec.ExecuteException) { + log.warn("can not parse kernel info") + } + } + } +} diff --git a/doc/op.gif b/doc/op.gif index e6d5928..7fc4c2d 100644 Binary files a/doc/op.gif and b/doc/op.gif differ diff --git a/external/extract_kernel.py b/external/extract_kernel.py new file mode 100755 index 0000000..9aa32f5 --- /dev/null +++ b/external/extract_kernel.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python2.7 +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +A tool to extract kernel information from a kernel image. +""" + +import argparse +import subprocess +import sys +import re + +CONFIG_PREFIX = b'IKCFG_ST' +GZIP_HEADER = b'\037\213\010' +COMPRESSION_ALGO = ( + (["gzip", "-d"], GZIP_HEADER), + (["xz", "-d"], b'\3757zXZ\000'), + (["bzip2", "-d"], b'BZh'), + (["lz4", "-d", "-l"], b'\002\041\114\030'), + + # These are not supported in the build system yet. + # (["unlzma"], b'\135\0\0\0'), + # (["lzop", "-d"], b'\211\114\132'), +) + +# "Linux version " UTS_RELEASE " (" LINUX_COMPILE_BY "@" +# LINUX_COMPILE_HOST ") (" LINUX_COMPILER ") " UTS_VERSION "\n"; +LINUX_BANNER_PREFIX = b'Linux version ' +LINUX_BANNER_REGEX = LINUX_BANNER_PREFIX + \ + r'([0-9]+[.][0-9]+[.][0-9]+).* \(.*@.*\) \(.*\) .*\n' + + +def get_version(input_bytes, start_idx): + null_idx = input_bytes.find('\x00', start_idx) + if null_idx < 0: + return None + linux_banner = input_bytes[start_idx:null_idx].decode() + mo = re.match(LINUX_BANNER_REGEX, linux_banner) + if mo: + return mo.group(1) + return None + + +def dump_version(input_bytes): + idx = 0 + while True: + idx = input_bytes.find(LINUX_BANNER_PREFIX, idx) + if idx < 0: + return None + + version = get_version(input_bytes, idx) + if version: + return version + + idx += len(LINUX_BANNER_PREFIX) + + +def dump_configs(input_bytes): + """ + Dump kernel configuration from input_bytes. This can be done when + CONFIG_IKCONFIG is enabled, which is a requirement on Treble devices. + + The kernel configuration is archived in GZip format right after the magic + string 'IKCFG_ST' in the built kernel. + """ + + # Search for magic string + GZip header + idx = input_bytes.find(CONFIG_PREFIX + GZIP_HEADER) + if idx < 0: + return None + + # Seek to the start of the archive + idx += len(CONFIG_PREFIX) + + sp = subprocess.Popen(["gzip", "-d", "-c"], stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + o, _ = sp.communicate(input=input_bytes[idx:]) + if sp.returncode == 1: # error + return None + + # success or trailing garbage warning + assert sp.returncode in (0, 2), sp.returncode + + return o + + +def try_decompress(cmd, search_bytes, input_bytes): + idx = input_bytes.find(search_bytes) + if idx < 0: + return None + + idx = 0 + sp = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + o, _ = sp.communicate(input=input_bytes[idx:]) + # ignore errors + return o + + +def decompress_dump(func, input_bytes): + """ + Run func(input_bytes) first; and if that fails (returns value evaluates to + False), then try different decompression algorithm before running func. + """ + o = func(input_bytes) + if o: + return o + for cmd, search_bytes in COMPRESSION_ALGO: + decompressed = try_decompress(cmd, search_bytes, input_bytes) + if decompressed: + o = func(decompressed) + if o: + return o + # Force decompress the whole file even if header doesn't match + decompressed = try_decompress(cmd, b"", input_bytes) + if decompressed: + o = func(decompressed) + if o: + return o + +def main(): + parser = argparse.ArgumentParser( + formatter_class=argparse.RawTextHelpFormatter, + description=__doc__ + + "\nThese algorithms are tried when decompressing the image:\n " + + " ".join(tup[0][0] for tup in COMPRESSION_ALGO)) + parser.add_argument('--input', + help='Input kernel image. If not specified, use stdin', + metavar='FILE', + type=argparse.FileType('rb'), + default=sys.stdin) + parser.add_argument('--output-configs', + help='If specified, write configs. Use stdout if no file ' + 'is specified.', + metavar='FILE', + nargs='?', + type=argparse.FileType('wb'), + const=sys.stdout) + parser.add_argument('--output-version', + help='If specified, write version. Use stdout if no file ' + 'is specified.', + metavar='FILE', + nargs='?', + type=argparse.FileType('wb'), + const=sys.stdout) + parser.add_argument('--tools', + help='Decompression tools to use. If not specified, PATH ' + 'is searched.', + metavar='ALGORITHM:EXECUTABLE', + nargs='*') + args = parser.parse_args() + + tools = {pair[0]: pair[1] + for pair in (token.split(':') for token in args.tools or [])} + for cmd, _ in COMPRESSION_ALGO: + if cmd[0] in tools: + cmd[0] = tools[cmd[0]] + + input_bytes = args.input.read() + + ret = 0 + if args.output_configs is not None: + o = decompress_dump(dump_configs, input_bytes) + if o: + args.output_configs.write(o) + else: + sys.stderr.write( + "Cannot extract kernel configs in {}\n".format(args.input.name)) + ret = 1 + if args.output_version is not None: + o = decompress_dump(dump_version, input_bytes) + if o: + args.output_version.write(o) + else: + sys.stderr.write( + "Cannot extract kernel versions in {}\n".format(args.input.name)) + ret = 1 + + return ret + + +if __name__ == '__main__': + exit(main())