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'(?P<release>(?P<version>[0-9]+[.][0-9]+[.][0-9]+).*) \(.*@.*\) \(.*\) .*\n' 44 45 46def get_from_release(input_bytes, start_idx, key): 47 null_idx = input_bytes.find('\x00', start_idx) 48 if null_idx < 0: 49 return None 50 try: 51 linux_banner = input_bytes[start_idx:null_idx].decode() 52 except UnicodeDecodeError: 53 return None 54 mo = re.match(LINUX_BANNER_REGEX, linux_banner) 55 if mo: 56 return mo.group(key) 57 return None 58 59 60def dump_from_release(input_bytes, key): 61 """ 62 Helper of dump_version and dump_release 63 """ 64 idx = 0 65 while True: 66 idx = input_bytes.find(LINUX_BANNER_PREFIX, idx) 67 if idx < 0: 68 return None 69 70 value = get_from_release(input_bytes, idx, key) 71 if value: 72 return value 73 74 idx += len(LINUX_BANNER_PREFIX) 75 76 77def dump_version(input_bytes): 78 """ 79 Dump kernel version, w.x.y, from input_bytes. Search for the string 80 "Linux version " and do pattern matching after it. See LINUX_BANNER_REGEX. 81 """ 82 return dump_from_release(input_bytes, "version") 83 84 85def dump_release(input_bytes): 86 """ 87 Dump kernel release, w.x.y-..., from input_bytes. Search for the string 88 "Linux version " and do pattern matching after it. See LINUX_BANNER_REGEX. 89 """ 90 return dump_from_release(input_bytes, "release") 91 92 93def dump_configs(input_bytes): 94 """ 95 Dump kernel configuration from input_bytes. This can be done when 96 CONFIG_IKCONFIG is enabled, which is a requirement on Treble devices. 97 98 The kernel configuration is archived in GZip format right after the magic 99 string 'IKCFG_ST' in the built kernel. 100 """ 101 102 # Search for magic string + GZip header 103 idx = input_bytes.find(CONFIG_PREFIX + GZIP_HEADER) 104 if idx < 0: 105 return None 106 107 # Seek to the start of the archive 108 idx += len(CONFIG_PREFIX) 109 110 sp = subprocess.Popen(["gzip", "-d", "-c"], stdin=subprocess.PIPE, 111 stdout=subprocess.PIPE, stderr=subprocess.PIPE) 112 o, _ = sp.communicate(input=input_bytes[idx:]) 113 if sp.returncode == 1: # error 114 return None 115 116 # success or trailing garbage warning 117 assert sp.returncode in (0, 2), sp.returncode 118 119 return o 120 121 122def try_decompress_bytes(cmd, input_bytes): 123 sp = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, 124 stderr=subprocess.PIPE) 125 o, _ = sp.communicate(input=input_bytes) 126 # ignore errors 127 return o 128 129 130def try_decompress(cmd, search_bytes, input_bytes): 131 idx = 0 132 while True: 133 idx = input_bytes.find(search_bytes, idx) 134 if idx < 0: 135 raise StopIteration() 136 137 yield try_decompress_bytes(cmd, input_bytes[idx:]) 138 idx += 1 139 140 141def decompress_dump(func, input_bytes): 142 """ 143 Run func(input_bytes) first; and if that fails (returns value evaluates to 144 False), then try different decompression algorithm before running func. 145 """ 146 o = func(input_bytes) 147 if o: 148 return o 149 for cmd, search_bytes in COMPRESSION_ALGO: 150 for decompressed in try_decompress(cmd, search_bytes, input_bytes): 151 if decompressed: 152 o = decompress_dump(func, decompressed) 153 if o: 154 return o 155 # Force decompress the whole file even if header doesn't match 156 decompressed = try_decompress_bytes(cmd, input_bytes) 157 if decompressed: 158 o = decompress_dump(func, decompressed) 159 if o: 160 return o 161 162 163def dump_to_file(f, dump_fn, input_bytes, desc): 164 """ 165 Call decompress_dump(dump_fn, input_bytes) and write to f. If it fails, return 166 False; otherwise return True. 167 """ 168 if f is not None: 169 o = decompress_dump(dump_fn, input_bytes) 170 if o: 171 f.write(o) 172 else: 173 sys.stderr.write( 174 "Cannot extract kernel {}".format(desc)) 175 return False 176 return True 177 178 179def main(): 180 parser = argparse.ArgumentParser( 181 formatter_class=argparse.RawTextHelpFormatter, 182 description=__doc__ + 183 "\nThese algorithms are tried when decompressing the image:\n " + 184 " ".join(tup[0][0] for tup in COMPRESSION_ALGO)) 185 parser.add_argument('--input', 186 help='Input kernel image. If not specified, use stdin', 187 metavar='FILE', 188 type=argparse.FileType('rb'), 189 default=sys.stdin) 190 parser.add_argument('--output-configs', 191 help='If specified, write configs. Use stdout if no file ' 192 'is specified.', 193 metavar='FILE', 194 nargs='?', 195 type=argparse.FileType('wb'), 196 const=sys.stdout) 197 parser.add_argument('--output-version', 198 help='If specified, write version. Use stdout if no file ' 199 'is specified.', 200 metavar='FILE', 201 nargs='?', 202 type=argparse.FileType('wb'), 203 const=sys.stdout) 204 parser.add_argument('--output-release', 205 help='If specified, write kernel release. Use stdout if ' 206 'no file is specified.', 207 metavar='FILE', 208 nargs='?', 209 type=argparse.FileType('wb'), 210 const=sys.stdout) 211 parser.add_argument('--tools', 212 help='Decompression tools to use. If not specified, PATH ' 213 'is searched.', 214 metavar='ALGORITHM:EXECUTABLE', 215 nargs='*') 216 args = parser.parse_args() 217 218 tools = {pair[0]: pair[1] 219 for pair in (token.split(':') for token in args.tools or [])} 220 for cmd, _ in COMPRESSION_ALGO: 221 if cmd[0] in tools: 222 cmd[0] = tools[cmd[0]] 223 224 input_bytes = args.input.read() 225 226 ret = 0 227 if not dump_to_file(args.output_configs, dump_configs, input_bytes, 228 "configs in {}".format(args.input.name)): 229 ret = 1 230 if not dump_to_file(args.output_version, dump_version, input_bytes, 231 "version in {}".format(args.input.name)): 232 ret = 1 233 if not dump_to_file(args.output_release, dump_release, input_bytes, 234 "kernel release in {}".format(args.input.name)): 235 ret = 1 236 237 return ret 238 239 240if __name__ == '__main__': 241 sys.exit(main()) 242