1#!/usr/bin/env python 2# 3# Copyright (C) 2018 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17""" 18A tool to extract kernel information from a kernel image. 19""" 20 21import argparse 22import subprocess 23import sys 24import re 25 26CONFIG_PREFIX = b'IKCFG_ST' 27GZIP_HEADER = b'\037\213\010' 28COMPRESSION_ALGO = ( 29 (["gzip", "-d"], GZIP_HEADER), 30 (["xz", "-d"], b'\3757zXZ\000'), 31 (["bzip2", "-d"], b'BZh'), 32 (["lz4", "-d", "-l"], b'\002\041\114\030'), 33 34 # These are not supported in the build system yet. 35 # (["unlzma"], b'\135\0\0\0'), 36 # (["lzop", "-d"], b'\211\114\132'), 37) 38 39# "Linux version " UTS_RELEASE " (" LINUX_COMPILE_BY "@" 40# LINUX_COMPILE_HOST ") (" LINUX_COMPILER ") " UTS_VERSION "\n"; 41LINUX_BANNER_PREFIX = b'Linux version ' 42LINUX_BANNER_REGEX = LINUX_BANNER_PREFIX + \ 43 r'([0-9]+[.][0-9]+[.][0-9]+).* \(.*@.*\) \(.*\) .*\n' 44 45 46def get_version(input_bytes, start_idx): 47 null_idx = input_bytes.find('\x00', start_idx) 48 if null_idx < 0: 49 return None 50 linux_banner = input_bytes[start_idx:null_idx].decode() 51 mo = re.match(LINUX_BANNER_REGEX, linux_banner) 52 if mo: 53 return mo.group(1) 54 return None 55 56 57def dump_version(input_bytes): 58 idx = 0 59 while True: 60 idx = input_bytes.find(LINUX_BANNER_PREFIX, idx) 61 if idx < 0: 62 return None 63 64 version = get_version(input_bytes, idx) 65 if version: 66 return version 67 68 idx += len(LINUX_BANNER_PREFIX) 69 70 71def dump_configs(input_bytes): 72 """ 73 Dump kernel configuration from input_bytes. This can be done when 74 CONFIG_IKCONFIG is enabled, which is a requirement on Treble devices. 75 76 The kernel configuration is archived in GZip format right after the magic 77 string 'IKCFG_ST' in the built kernel. 78 """ 79 80 # Search for magic string + GZip header 81 idx = input_bytes.find(CONFIG_PREFIX + GZIP_HEADER) 82 if idx < 0: 83 return None 84 85 # Seek to the start of the archive 86 idx += len(CONFIG_PREFIX) 87 88 sp = subprocess.Popen(["gzip", "-d", "-c"], stdin=subprocess.PIPE, 89 stdout=subprocess.PIPE, stderr=subprocess.PIPE) 90 o, _ = sp.communicate(input=input_bytes[idx:]) 91 if sp.returncode == 1: # error 92 return None 93 94 # success or trailing garbage warning 95 assert sp.returncode in (0, 2), sp.returncode 96 97 return o 98 99 100def try_decompress(cmd, search_bytes, input_bytes): 101 idx = input_bytes.find(search_bytes) 102 if idx < 0: 103 return None 104 105 idx = 0 106 sp = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, 107 stderr=subprocess.PIPE) 108 o, _ = sp.communicate(input=input_bytes[idx:]) 109 # ignore errors 110 return o 111 112 113def decompress_dump(func, input_bytes): 114 """ 115 Run func(input_bytes) first; and if that fails (returns value evaluates to 116 False), then try different decompression algorithm before running func. 117 """ 118 o = func(input_bytes) 119 if o: 120 return o 121 for cmd, search_bytes in COMPRESSION_ALGO: 122 decompressed = try_decompress(cmd, search_bytes, input_bytes) 123 if decompressed: 124 o = func(decompressed) 125 if o: 126 return o 127 # Force decompress the whole file even if header doesn't match 128 decompressed = try_decompress(cmd, b"", input_bytes) 129 if decompressed: 130 o = func(decompressed) 131 if o: 132 return o 133 134def main(): 135 parser = argparse.ArgumentParser( 136 formatter_class=argparse.RawTextHelpFormatter, 137 description=__doc__ + 138 "\nThese algorithms are tried when decompressing the image:\n " + 139 " ".join(tup[0][0] for tup in COMPRESSION_ALGO)) 140 parser.add_argument('--input', 141 help='Input kernel image. If not specified, use stdin', 142 metavar='FILE', 143 type=argparse.FileType('rb'), 144 default=sys.stdin) 145 parser.add_argument('--output-configs', 146 help='If specified, write configs. Use stdout if no file ' 147 'is specified.', 148 metavar='FILE', 149 nargs='?', 150 type=argparse.FileType('wb'), 151 const=sys.stdout) 152 parser.add_argument('--output-version', 153 help='If specified, write version. Use stdout if no file ' 154 'is specified.', 155 metavar='FILE', 156 nargs='?', 157 type=argparse.FileType('wb'), 158 const=sys.stdout) 159 parser.add_argument('--tools', 160 help='Decompression tools to use. If not specified, PATH ' 161 'is searched.', 162 metavar='ALGORITHM:EXECUTABLE', 163 nargs='*') 164 args = parser.parse_args() 165 166 tools = {pair[0]: pair[1] 167 for pair in (token.split(':') for token in args.tools or [])} 168 for cmd, _ in COMPRESSION_ALGO: 169 if cmd[0] in tools: 170 cmd[0] = tools[cmd[0]] 171 172 input_bytes = args.input.read() 173 174 ret = 0 175 if args.output_configs is not None: 176 o = decompress_dump(dump_configs, input_bytes) 177 if o: 178 args.output_configs.write(o) 179 else: 180 sys.stderr.write( 181 "Cannot extract kernel configs in {}".format(args.input.name)) 182 ret = 1 183 if args.output_version is not None: 184 o = decompress_dump(dump_version, input_bytes) 185 if o: 186 args.output_version.write(o) 187 else: 188 sys.stderr.write( 189 "Cannot extract kernel versions in {}".format(args.input.name)) 190 ret = 1 191 192 return ret 193 194 195if __name__ == '__main__': 196 exit(main()) 197