1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150
  151
  152
  153
  154
  155
  156
  157
  158
  159
  160
  161
  162
  163
  164
  165
  166
  167
  168
  169
  170
  171
  172
  173
  174
  175
  176
  177
  178
  179
  180
  181
  182
  183
  184
  185
  186
  187
  188
  189
  190
  191
  192
  193
  194
  195
  196
  197
  198
  199
  200
  201
  202
  203
  204
  205
  206
  207
  208
  209
  210
  211
  212
  213
  214
  215
  216
  217
  218
  219
  220
  221
  222
  223
  224
  225
  226
  227
  228
  229
  230
  231
  232
  233
  234
  235
  236
  237
  238
  239
  240
  241
  242
  243
  244
  245
  246
  247
  248
  249
  250
  251
  252
  253
  254
  255
  256
  257
  258
  259
  260
  261
  262
  263
  264
  265
  266
  267
  268
  269
  270
  271
  272
  273
  274
  275
  276
  277
  278
  279
  280
  281
  282
  283

build / android / gyp / extract_unwind_tables.py [blame]

#!/usr/bin/env python3
# Copyright 2018 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Extracts the unwind tables in from breakpad symbol files

Runs dump_syms on the given binary file and extracts the CFI data into the
given output file.
The output file is a binary file containing CFI rows ordered based on function
address. The output file only contains rows that match the most popular rule
type in CFI table, to reduce the output size and specify data in compact format.
See doc https://github.com/google/breakpad/blob/main/docs/symbol_files.md.
1. The CFA rules should be of postfix form "SP <val> +".
2. The RA rules should be of postfix form "CFA <val> + ^".
Note: breakpad represents dereferencing address with '^' operator.

The output file has 2 tables UNW_INDEX and UNW_DATA, inspired from ARM EHABI
format. The first table contains function addresses and an index into the
UNW_DATA table. The second table contains one or more rows for the function
unwind information.

The output file starts with 4 bytes counting the number of entries in UNW_INDEX.
Then UNW_INDEX table and UNW_DATA table.

UNW_INDEX contains two columns of N rows each, where N is the number of
functions.
  1. First column 4 byte rows of all the function start address as offset from
     start of the binary, in sorted order.
  2. For each function addr, the second column contains 2 byte indices in order.
     The indices are offsets (in count of 2 bytes) of the CFI data from start of
     UNW_DATA.
The last entry in the table always contains CANT_UNWIND index to specify the
end address of the last function.

UNW_DATA contains data of all the functions. Each function data contains N rows.
The data found at the address pointed from UNW_INDEX will be:
  2 bytes: N - number of rows that belong to current function.
  N * 4 bytes: N rows of data. 16 bits : Address offset from function start.
                               14 bits : CFA offset / 4.
                                2 bits : RA offset / 4.

The function is not added to the unwind table in following conditions:
C1. If length of the function code (number of instructions) is greater than
    0xFFFF (2 byte address span). This is because we use 16 bits to refer to
    offset of instruction from start of the address.
C2. If the function moves the SP by more than 0xFFFF bytes. This is because we
    use 14 bits to denote CFA offset (last 2 bits are 0).
C3. If the Return Address is stored at an offset >= 16 from the CFA. Some
    functions which have variable arguments can have offset upto 16.
    TODO(ssid): We can actually store offset 16 by subtracting 1 from RA/4 since
    we never have 0.
C4: Some functions do not have unwind information defined in dwarf info. These
    functions have index value CANT_UNWIND(0xFFFF) in UNW_INDEX table.


Usage:
  extract_unwind_tables.py --input_path [root path to unstripped chrome.so]
      --output_path [output path] --dump_syms_path [path to dump_syms binary]
"""

import argparse
import re
import struct
import subprocess
import sys
import tempfile


_CFA_REG = '.cfa'
_RA_REG = '.ra'

_ADDR_ENTRY = 0
_LENGTH_ENTRY = 1

_CANT_UNWIND = 0xFFFF


def _Write4Bytes(output_file, val):
  """Writes a 32 bit unsigned integer to the given output file."""
  output_file.write(struct.pack('<L', val));


def _Write2Bytes(output_file, val):
  """Writes a 16 bit unsigned integer to the given output file."""
  output_file.write(struct.pack('<H', val));


def _FindRuleForRegister(cfi_row, reg):
  """Returns the postfix expression as string for a given register.

  Breakpad CFI row format specifies rules for unwinding each register in postfix
  expression form separated by space. Each rule starts with register name and a
  colon. Eg: "CFI R1: <rule> R2: <rule>".
  """
  out = []
  found_register = False
  for part in cfi_row:
    if found_register:
      if part[-1] == ':':
        break
      out.append(part)
    elif part == reg + ':':
      found_register = True
  return ' '.join(out)


def _GetCfaAndRaOffset(cfi_row):
  """Returns a tuple with 2 numbers (cfa_offset, ra_offset).

  Returns right values if rule matches the predefined criteria. Returns (0, 0)
  otherwise. The criteria for CFA rule is postfix form "SP <val> +" and RA rule
  is postfix form "CFA -<val> + ^".
  """
  cfa_offset = 0
  ra_offset = 0
  cfa_rule = _FindRuleForRegister(cfi_row, _CFA_REG)
  ra_rule = _FindRuleForRegister(cfi_row, _RA_REG)
  if cfa_rule and re.match(r'sp [0-9]+ \+', cfa_rule):
    cfa_offset = int(cfa_rule.split()[1], 10)
  if ra_rule:
    if not re.match(r'.cfa -[0-9]+ \+ \^', ra_rule):
      return (0, 0)
    ra_offset = -1 * int(ra_rule.split()[1], 10)
  return (cfa_offset, ra_offset)


def _GetAllCfiRows(symbol_file):
  """Returns parsed CFI data from given symbol_file.

  Each entry in the cfi data dictionary returned is a map from function start
  address to array of function rows, starting with FUNCTION type, followed by
  one or more CFI rows.
  """
  cfi_data = {}
  current_func = []
  for line in symbol_file:
    line = line.decode('utf8')
    if 'STACK CFI' not in line:
      continue

    parts = line.split()
    data = {}
    if parts[2] == 'INIT':
      # Add the previous function to the output
      if len(current_func) > 1:
        cfi_data[current_func[0][_ADDR_ENTRY]] = current_func
      current_func = []

      # The function line is of format "STACK CFI INIT <addr> <length> ..."
      data[_ADDR_ENTRY] = int(parts[3], 16)
      data[_LENGTH_ENTRY] = int(parts[4], 16)

      # Condition C1: Skip if length is large.
      if data[_LENGTH_ENTRY] == 0 or data[_LENGTH_ENTRY] > 0xffff:
        continue  # Skip the current function.
    else:
      # The current function is skipped.
      if len(current_func) == 0:
        continue

      # The CFI row is of format "STACK CFI <addr> .cfa: <expr> .ra: <expr> ..."
      data[_ADDR_ENTRY] = int(parts[2], 16)
      (data[_CFA_REG], data[_RA_REG]) = _GetCfaAndRaOffset(parts)

      # Condition C2 and C3: Skip based on limits on offsets.
      if data[_CFA_REG] == 0 or data[_RA_REG] >= 16 or data[_CFA_REG] > 0xffff:
        current_func = []
        continue
      assert data[_CFA_REG] % 4 == 0
      # Since we skipped functions with code size larger than 0xffff, we should
      # have no function offset larger than the same value.
      assert data[_ADDR_ENTRY] - current_func[0][_ADDR_ENTRY] < 0xffff

    if data[_ADDR_ENTRY] == 0:
      # Skip current function, delete all previous entries.
      current_func = []
      continue
    assert data[_ADDR_ENTRY] % 2 == 0
    current_func.append(data)

  # Condition C4: Skip function without CFI rows.
  if len(current_func) > 1:
    cfi_data[current_func[0][_ADDR_ENTRY]] = current_func
  return cfi_data


def _WriteCfiData(cfi_data, out_file):
  """Writes the CFI data in defined format to out_file."""
  # Stores the final data that will be written to UNW_DATA table, in order
  # with 2 byte items.
  unw_data = []

  # Represent all the CFI data of functions as set of numbers and map them to an
  # index in the |unw_data|. This index is later written to the UNW_INDEX table
  # for each function. This map is used to find index of the data for functions.
  data_to_index = {}
  # Store mapping between the functions to the index.
  func_addr_to_index = {}
  previous_func_end = 0
  for addr, function in sorted(cfi_data.items()):
    # Add an empty function entry when functions CFIs are missing between 2
    # functions.
    if previous_func_end != 0 and addr - previous_func_end  > 4:
      func_addr_to_index[previous_func_end + 2] = _CANT_UNWIND
    previous_func_end = addr + cfi_data[addr][0][_LENGTH_ENTRY]

    assert len(function) > 1
    func_data_arr = []
    func_data = 0
    # The first row contains the function address and length. The rest of the
    # rows have CFI data. Create function data array as given in the format.
    for row in function[1:]:
      addr_offset = row[_ADDR_ENTRY] - addr
      cfa_offset = (row[_CFA_REG]) | (row[_RA_REG] // 4)

      func_data_arr.append(addr_offset)
      func_data_arr.append(cfa_offset)

    # Consider all the rows in the data as one large integer and add it as a key
    # to the |data_to_index|.
    for data in func_data_arr:
      func_data = (func_data << 16) | data

    row_count = len(func_data_arr) // 2
    if func_data not in data_to_index:
      # When data is not found, create a new index = len(unw_data), and write
      # the data to |unw_data|.
      index = len(unw_data)
      data_to_index[func_data] = index
      unw_data.append(row_count)
      for row in func_data_arr:
        unw_data.append(row)
    else:
      # If the data was found, then use the same index for the function.
      index = data_to_index[func_data]
      assert row_count == unw_data[index]
    func_addr_to_index[addr] = data_to_index[func_data]

  # Mark the end end of last function entry.
  func_addr_to_index[previous_func_end + 2] = _CANT_UNWIND

  # Write the size of UNW_INDEX file in bytes.
  _Write4Bytes(out_file, len(func_addr_to_index))

  # Write the UNW_INDEX table. First list of addresses and then indices.
  sorted_unw_index = sorted(func_addr_to_index.items())
  for addr, index in sorted_unw_index:
    _Write4Bytes(out_file, addr)
  for addr, index in sorted_unw_index:
    _Write2Bytes(out_file, index)

  # Write the UNW_DATA table.
  for data in unw_data:
    _Write2Bytes(out_file, data)


def main():
  parser = argparse.ArgumentParser()
  parser.add_argument(
      '--input_path', required=True,
      help='The input path of the unstripped binary')
  parser.add_argument(
      '--output_path', required=True,
      help='The path of the output file')
  parser.add_argument(
      '--dump_syms_path', required=True,
      help='The path of the dump_syms binary')

  args = parser.parse_args()
  cmd = ['./' + args.dump_syms_path, args.input_path, '-v']
  proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
  cfi_data = _GetAllCfiRows(proc.stdout)
  if proc.wait():
    sys.stderr.write('dump_syms exited with code {} after {} symbols\n'.format(
        proc.returncode, len(cfi_data)))
    sys.exit(proc.returncode)
  with open(args.output_path, 'wb') as out_file:
    _WriteCfiData(cfi_data, out_file)


if __name__ == '__main__':
  main()