123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310 |
- #!/usr/bin/env python
- # Copyright 2024 Google LLC
- #
- # 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.
- from __future__ import with_statement
- from struct import pack, unpack
- import os
- import os.path
- import sys
- import time
- from subprocess import Popen, PIPE
- from shutil import copy2
- from binascii import crc32
- from struct import pack
- from pbpack import ResourcePack
- import stm32_crc
- # Pebble App Metadata Struct
- # These are offsets of the PebbleProcessInfo struct in src/fw/app_management/pebble_process_info.h
- HEADER_ADDR = 0x0 # 8 bytes
- STRUCT_VERSION_ADDR = 0x8 # 2 bytes
- SDK_VERSION_ADDR = 0xa # 2 bytes
- APP_VERSION_ADDR = 0xc # 2 bytes
- LOAD_SIZE_ADDR = 0xe # 2 bytes
- OFFSET_ADDR = 0x10 # 4 bytes
- CRC_ADDR = 0x14 # 4 bytes
- NAME_ADDR = 0x18 # 32 bytes
- COMPANY_ADDR = 0x38 # 32 bytes
- ICON_RES_ID_ADDR = 0x58 # 4 bytes
- JUMP_TABLE_ADDR = 0x5c # 4 bytes
- FLAGS_ADDR = 0x60 # 4 bytes
- NUM_RELOC_ENTRIES_ADDR = 0x64 # 4 bytes
- UUID_ADDR = 0x68 # 16 bytes
- RESOURCE_CRC_ADDR = 0x78 # 4 bytes
- RESOURCE_TIMESTAMP_ADDR = 0x7c # 4 bytes
- VIRTUAL_SIZE_ADDR = 0x80 # 2 bytes
- STRUCT_SIZE_BYTES = 0x82
- # Pebble App Flags
- # These are PebbleAppFlags from src/fw/app_management/pebble_process_info.h
- PROCESS_INFO_STANDARD_APP = (0)
- PROCESS_INFO_WATCH_FACE = (1 << 0)
- PROCESS_INFO_VISIBILITY_HIDDEN = (1 << 1)
- PROCESS_INFO_VISIBILITY_SHOWN_ON_COMMUNICATION = (1 << 2)
- PROCESS_INFO_ALLOW_JS = (1 << 3)
- PROCESS_INFO_HAS_WORKER = (1 << 4)
- # Max app size, including the struct and reloc table
- # Note that even if the app is smaller than this, it still may be too big, as it needs to share this
- # space with applib/ which changes in size from release to release.
- MAX_APP_BINARY_SIZE = 0x10000
- # This number is a rough estimate, but should not be less than the available space.
- # Currently, app_state uses up a small part of the app space.
- # See also APP_RAM in stm32f2xx_flash_fw.ld and APP in pebble_app.ld.
- MAX_APP_MEMORY_SIZE = 24 * 1024
- # This number is a rough estimate, but should not be less than the available space.
- # Currently, worker_state uses up a small part of the worker space.
- # See also WORKER_RAM in stm32f2xx_flash_fw.ld
- MAX_WORKER_MEMORY_SIZE = 10 * 1024
- ENTRY_PT_SYMBOL = 'main'
- JUMP_TABLE_ADDR_SYMBOL = 'pbl_table_addr'
- DEBUG = False
- class InvalidBinaryError(Exception):
- pass
- def inject_metadata(target_binary, target_elf, resources_file, timestamp, allow_js=False,
- has_worker=False):
- if target_binary[-4:] != '.bin':
- raise Exception("Invalid filename <%s>! The filename should end in .bin" % target_binary)
- def get_nm_output(elf_file):
- nm_process = Popen(['arm-none-eabi-nm', elf_file], stdout=PIPE)
- # Popen.communicate returns a tuple of (stdout, stderr)
- nm_output = nm_process.communicate()[0].decode("utf8")
- if not nm_output:
- raise InvalidBinaryError()
- nm_output = [ line.split() for line in nm_output.splitlines() ]
- return nm_output
- def get_symbol_addr(nm_output, symbol):
- # nm output looks like the following...
- #
- # U _ITM_registerTMCloneTable
- # 00000084 t jump_to_pbl_function
- # U _Jv_RegisterClasses
- # 0000009c T main
- # 00000130 T memset
- #
- # We don't care about the lines that only have two columns, they're not functions.
- for sym in nm_output:
- if symbol == sym[-1] and len(sym) == 3:
- return int(sym[0], 16)
- raise Exception("Could not locate symbol <%s> in binary! Failed to inject app metadata" %
- (symbol))
- def get_virtual_size(elf_file):
- """ returns the virtual size (static memory usage, .text + .data + .bss) in bytes """
- readelf_bss_process = Popen("arm-none-eabi-readelf -S '%s'" % elf_file,
- shell=True, stdout=PIPE)
- readelf_bss_output = readelf_bss_process.communicate()[0].decode("utf8")
- # readelf -S output looks like the following...
- #
- # [Nr] Name Type Addr Off Size ES Flg Lk Inf Al
- # [ 0] NULL 00000000 000000 000000 00 0 0 0
- # [ 1] .header PROGBITS 00000000 008000 000082 00 A 0 0 1
- # [ 2] .text PROGBITS 00000084 008084 0006be 00 AX 0 0 4
- # [ 3] .rel.text REL 00000000 00b66c 0004d0 08 23 2 4
- # [ 4] .data PROGBITS 00000744 008744 000004 00 WA 0 0 4
- # [ 5] .bss NOBITS 00000748 008748 000054 00 WA 0 0 4
- last_section_end_addr = 0
- # Find the .bss section and calculate the size based on the end of the .bss section
- for line in readelf_bss_output.splitlines():
- if len(line) < 10:
- continue
- # Carve off the first column, since it sometimes has a space in it which screws up the
- # split.
- if not ']' in line:
- continue
- line = line[line.index(']') + 1:]
- columns = line.split()
- if len(columns) < 6:
- continue
- if columns[0] == '.bss':
- addr = int(columns[2], 16)
- size = int(columns[4], 16)
- last_section_end_addr = addr + size
- elif columns[0] == '.data' and last_section_end_addr == 0:
- addr = int(columns[2], 16)
- size = int(columns[4], 16)
- last_section_end_addr = addr + size
- if last_section_end_addr != 0:
- return last_section_end_addr
- sys.stderr.writeline("Failed to parse ELF sections while calculating the virtual size\n")
- sys.stderr.write(readelf_bss_output)
- raise Exception("Failed to parse ELF sections while calculating the virtual size")
- def get_relocate_entries(elf_file):
- """ returns a list of all the locations requiring an offset"""
- # TODO: insert link to the wiki page I'm about to write about PIC and relocatable values
- entries = []
- # get the .data locations
- readelf_relocs_process = Popen(['arm-none-eabi-readelf', '-r', elf_file], stdout=PIPE)
- readelf_relocs_output = readelf_relocs_process.communicate()[0].decode("utf8")
- lines = readelf_relocs_output.splitlines()
- i = 0
- reading_section = False
- while i < len(lines):
- if not reading_section:
- # look for the next section
- if lines[i].startswith("Relocation section '.rel.data"):
- reading_section = True
- i += 1 # skip the column title section
- else:
- if len(lines[i]) == 0:
- # end of the section
- reading_section = False
- else:
- entries.append(int(lines[i].split(' ')[0], 16))
- i += 1
- # get any Global Offset Table (.got) entries
- readelf_relocs_process = Popen(['arm-none-eabi-readelf', '--sections', elf_file],
- stdout=PIPE)
- readelf_relocs_output = readelf_relocs_process.communicate()[0].decode("utf8")
- lines = readelf_relocs_output.splitlines()
- for line in lines:
- # We shouldn't need to do anything with the Procedure Linkage Table since we don't
- # actually export functions
- if '.got' in line and '.got.plt' not in line:
- words = line.split(' ')
- while '' in words:
- words.remove('')
- section_label_idx = words.index('.got')
- addr = int(words[section_label_idx + 2], 16)
- length = int(words[section_label_idx + 4], 16)
- for i in range(addr, addr + length, 4):
- entries.append(i)
- break
- return entries
- nm_output = get_nm_output(target_elf)
- try:
- app_entry_address = get_symbol_addr(nm_output, ENTRY_PT_SYMBOL)
- except:
- raise Exception("Missing app entry point! Must be `int main(void) { ... }` ")
- jump_table_address = get_symbol_addr(nm_output, JUMP_TABLE_ADDR_SYMBOL)
- reloc_entries = get_relocate_entries(target_elf)
- statinfo = os.stat(target_binary)
- app_load_size = statinfo.st_size
- if resources_file is not None:
- with open(resources_file, 'rb') as f:
- pbpack = ResourcePack.deserialize(f, is_system=False)
- resource_crc = pbpack.get_content_crc()
- else:
- resource_crc = 0
- if DEBUG:
- copy2(target_binary, target_binary + ".orig")
- with open(target_binary, 'r+b') as f:
- total_app_image_size = app_load_size + (len(reloc_entries) * 4)
- if total_app_image_size > MAX_APP_BINARY_SIZE:
- raise Exception("App image size is %u (app %u relocation table %u). Must be smaller "
- "than %u bytes" % (total_app_image_size,
- app_load_size,
- len(reloc_entries) * 4,
- MAX_APP_BINARY_SIZE))
- def read_value_at_offset(offset, format_str, size):
- f.seek(offset)
- return unpack(format_str, f.read(size))
- app_bin = f.read()
- app_crc = stm32_crc.crc32(app_bin[STRUCT_SIZE_BYTES:])
- [app_flags] = read_value_at_offset(FLAGS_ADDR, '<L', 4)
- if allow_js:
- app_flags = app_flags | PROCESS_INFO_ALLOW_JS
- if has_worker:
- app_flags = app_flags | PROCESS_INFO_HAS_WORKER
- app_virtual_size = get_virtual_size(target_elf)
- struct_changes = {
- 'load_size' : app_load_size,
- 'entry_point' : "0x%08x" % app_entry_address,
- 'symbol_table' : "0x%08x" % jump_table_address,
- 'flags' : app_flags,
- 'crc' : "0x%08x" % app_crc,
- 'num_reloc_entries': "0x%08x" % len(reloc_entries),
- 'resource_crc' : "0x%08x" % resource_crc,
- 'timestamp' : timestamp,
- 'virtual_size': app_virtual_size
- }
- def write_value_at_offset(offset, format_str, value):
- f.seek(offset)
- f.write(pack(format_str, value))
- write_value_at_offset(LOAD_SIZE_ADDR, '<H', app_load_size)
- write_value_at_offset(OFFSET_ADDR, '<L', app_entry_address)
- write_value_at_offset(CRC_ADDR, '<L', app_crc)
- write_value_at_offset(RESOURCE_CRC_ADDR, '<L', resource_crc)
- write_value_at_offset(RESOURCE_TIMESTAMP_ADDR, '<L', timestamp)
- write_value_at_offset(JUMP_TABLE_ADDR, '<L', jump_table_address)
- write_value_at_offset(FLAGS_ADDR, '<L', app_flags)
- write_value_at_offset(NUM_RELOC_ENTRIES_ADDR, '<L', len(reloc_entries))
- write_value_at_offset(VIRTUAL_SIZE_ADDR, "<H", app_virtual_size)
- # Write the reloc_entries past the end of the binary. This expands the size of the binary,
- # but this new stuff won't actually be loaded into ram.
- f.seek(app_load_size)
- for entry in reloc_entries:
- f.write(pack('<L', entry))
- f.flush()
- return struct_changes
|