#!/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] 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] # 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. Two leading spaces, a square bracket, 2 digits (with space padding), # a second square brack is 6 line = line[6:] 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] 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] 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, '