Home | History | Annotate | Download | only in tools
      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 """
     18 A tool to extract kernel information from a kernel image.
     19 """
     20 
     21 import argparse
     22 import subprocess
     23 import sys
     24 import re
     25 
     26 CONFIG_PREFIX = b'IKCFG_ST'
     27 GZIP_HEADER = b'\037\213\010'
     28 COMPRESSION_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";
     41 LINUX_BANNER_PREFIX = b'Linux version '
     42 LINUX_BANNER_REGEX = LINUX_BANNER_PREFIX + \
     43     r'([0-9]+[.][0-9]+[.][0-9]+).* \(.*@.*\) \(.*\) .*\n'
     44 
     45 
     46 def 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 
     57 def 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 
     71 def 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 
    100 def 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 
    113 def 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 
    134 def 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 
    195 if __name__ == '__main__':
    196   exit(main())
    197