inject_metadata.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. #!/usr/bin/env python
  2. # Copyright 2024 Google LLC
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. from __future__ import with_statement
  16. from struct import pack, unpack
  17. import os
  18. import os.path
  19. import sys
  20. import time
  21. from subprocess import Popen, PIPE
  22. from shutil import copy2
  23. from binascii import crc32
  24. from struct import pack
  25. from pbpack import ResourcePack
  26. import stm32_crc
  27. # Pebble App Metadata Struct
  28. # These are offsets of the PebbleProcessInfo struct in src/fw/app_management/pebble_process_info.h
  29. HEADER_ADDR = 0x0 # 8 bytes
  30. STRUCT_VERSION_ADDR = 0x8 # 2 bytes
  31. SDK_VERSION_ADDR = 0xa # 2 bytes
  32. APP_VERSION_ADDR = 0xc # 2 bytes
  33. LOAD_SIZE_ADDR = 0xe # 2 bytes
  34. OFFSET_ADDR = 0x10 # 4 bytes
  35. CRC_ADDR = 0x14 # 4 bytes
  36. NAME_ADDR = 0x18 # 32 bytes
  37. COMPANY_ADDR = 0x38 # 32 bytes
  38. ICON_RES_ID_ADDR = 0x58 # 4 bytes
  39. JUMP_TABLE_ADDR = 0x5c # 4 bytes
  40. FLAGS_ADDR = 0x60 # 4 bytes
  41. NUM_RELOC_ENTRIES_ADDR = 0x64 # 4 bytes
  42. UUID_ADDR = 0x68 # 16 bytes
  43. RESOURCE_CRC_ADDR = 0x78 # 4 bytes
  44. RESOURCE_TIMESTAMP_ADDR = 0x7c # 4 bytes
  45. VIRTUAL_SIZE_ADDR = 0x80 # 2 bytes
  46. STRUCT_SIZE_BYTES = 0x82
  47. # Pebble App Flags
  48. # These are PebbleAppFlags from src/fw/app_management/pebble_process_info.h
  49. PROCESS_INFO_STANDARD_APP = (0)
  50. PROCESS_INFO_WATCH_FACE = (1 << 0)
  51. PROCESS_INFO_VISIBILITY_HIDDEN = (1 << 1)
  52. PROCESS_INFO_VISIBILITY_SHOWN_ON_COMMUNICATION = (1 << 2)
  53. PROCESS_INFO_ALLOW_JS = (1 << 3)
  54. PROCESS_INFO_HAS_WORKER = (1 << 4)
  55. # Max app size, including the struct and reloc table
  56. # Note that even if the app is smaller than this, it still may be too big, as it needs to share this
  57. # space with applib/ which changes in size from release to release.
  58. MAX_APP_BINARY_SIZE = 0x10000
  59. # This number is a rough estimate, but should not be less than the available space.
  60. # Currently, app_state uses up a small part of the app space.
  61. # See also APP_RAM in stm32f2xx_flash_fw.ld and APP in pebble_app.ld.
  62. MAX_APP_MEMORY_SIZE = 24 * 1024
  63. # This number is a rough estimate, but should not be less than the available space.
  64. # Currently, worker_state uses up a small part of the worker space.
  65. # See also WORKER_RAM in stm32f2xx_flash_fw.ld
  66. MAX_WORKER_MEMORY_SIZE = 10 * 1024
  67. ENTRY_PT_SYMBOL = 'main'
  68. JUMP_TABLE_ADDR_SYMBOL = 'pbl_table_addr'
  69. DEBUG = False
  70. class InvalidBinaryError(Exception):
  71. pass
  72. def inject_metadata(target_binary, target_elf, resources_file, timestamp, allow_js=False,
  73. has_worker=False):
  74. if target_binary[-4:] != '.bin':
  75. raise Exception("Invalid filename <%s>! The filename should end in .bin" % target_binary)
  76. def get_nm_output(elf_file):
  77. nm_process = Popen(['arm-none-eabi-nm', elf_file], stdout=PIPE)
  78. # Popen.communicate returns a tuple of (stdout, stderr)
  79. nm_output = nm_process.communicate()[0].decode("utf8")
  80. if not nm_output:
  81. raise InvalidBinaryError()
  82. nm_output = [ line.split() for line in nm_output.splitlines() ]
  83. return nm_output
  84. def get_symbol_addr(nm_output, symbol):
  85. # nm output looks like the following...
  86. #
  87. # U _ITM_registerTMCloneTable
  88. # 00000084 t jump_to_pbl_function
  89. # U _Jv_RegisterClasses
  90. # 0000009c T main
  91. # 00000130 T memset
  92. #
  93. # We don't care about the lines that only have two columns, they're not functions.
  94. for sym in nm_output:
  95. if symbol == sym[-1] and len(sym) == 3:
  96. return int(sym[0], 16)
  97. raise Exception("Could not locate symbol <%s> in binary! Failed to inject app metadata" %
  98. (symbol))
  99. def get_virtual_size(elf_file):
  100. """ returns the virtual size (static memory usage, .text + .data + .bss) in bytes """
  101. readelf_bss_process = Popen("arm-none-eabi-readelf -S '%s'" % elf_file,
  102. shell=True, stdout=PIPE)
  103. readelf_bss_output = readelf_bss_process.communicate()[0].decode("utf8")
  104. # readelf -S output looks like the following...
  105. #
  106. # [Nr] Name Type Addr Off Size ES Flg Lk Inf Al
  107. # [ 0] NULL 00000000 000000 000000 00 0 0 0
  108. # [ 1] .header PROGBITS 00000000 008000 000082 00 A 0 0 1
  109. # [ 2] .text PROGBITS 00000084 008084 0006be 00 AX 0 0 4
  110. # [ 3] .rel.text REL 00000000 00b66c 0004d0 08 23 2 4
  111. # [ 4] .data PROGBITS 00000744 008744 000004 00 WA 0 0 4
  112. # [ 5] .bss NOBITS 00000748 008748 000054 00 WA 0 0 4
  113. last_section_end_addr = 0
  114. # Find the .bss section and calculate the size based on the end of the .bss section
  115. for line in readelf_bss_output.splitlines():
  116. if len(line) < 10:
  117. continue
  118. # Carve off the first column, since it sometimes has a space in it which screws up the
  119. # split.
  120. if not ']' in line:
  121. continue
  122. line = line[line.index(']') + 1:]
  123. columns = line.split()
  124. if len(columns) < 6:
  125. continue
  126. if columns[0] == '.bss':
  127. addr = int(columns[2], 16)
  128. size = int(columns[4], 16)
  129. last_section_end_addr = addr + size
  130. elif columns[0] == '.data' and last_section_end_addr == 0:
  131. addr = int(columns[2], 16)
  132. size = int(columns[4], 16)
  133. last_section_end_addr = addr + size
  134. if last_section_end_addr != 0:
  135. return last_section_end_addr
  136. sys.stderr.writeline("Failed to parse ELF sections while calculating the virtual size\n")
  137. sys.stderr.write(readelf_bss_output)
  138. raise Exception("Failed to parse ELF sections while calculating the virtual size")
  139. def get_relocate_entries(elf_file):
  140. """ returns a list of all the locations requiring an offset"""
  141. # TODO: insert link to the wiki page I'm about to write about PIC and relocatable values
  142. entries = []
  143. # get the .data locations
  144. readelf_relocs_process = Popen(['arm-none-eabi-readelf', '-r', elf_file], stdout=PIPE)
  145. readelf_relocs_output = readelf_relocs_process.communicate()[0].decode("utf8")
  146. lines = readelf_relocs_output.splitlines()
  147. i = 0
  148. reading_section = False
  149. while i < len(lines):
  150. if not reading_section:
  151. # look for the next section
  152. if lines[i].startswith("Relocation section '.rel.data"):
  153. reading_section = True
  154. i += 1 # skip the column title section
  155. else:
  156. if len(lines[i]) == 0:
  157. # end of the section
  158. reading_section = False
  159. else:
  160. entries.append(int(lines[i].split(' ')[0], 16))
  161. i += 1
  162. # get any Global Offset Table (.got) entries
  163. readelf_relocs_process = Popen(['arm-none-eabi-readelf', '--sections', elf_file],
  164. stdout=PIPE)
  165. readelf_relocs_output = readelf_relocs_process.communicate()[0].decode("utf8")
  166. lines = readelf_relocs_output.splitlines()
  167. for line in lines:
  168. # We shouldn't need to do anything with the Procedure Linkage Table since we don't
  169. # actually export functions
  170. if '.got' in line and '.got.plt' not in line:
  171. words = line.split(' ')
  172. while '' in words:
  173. words.remove('')
  174. section_label_idx = words.index('.got')
  175. addr = int(words[section_label_idx + 2], 16)
  176. length = int(words[section_label_idx + 4], 16)
  177. for i in range(addr, addr + length, 4):
  178. entries.append(i)
  179. break
  180. return entries
  181. nm_output = get_nm_output(target_elf)
  182. try:
  183. app_entry_address = get_symbol_addr(nm_output, ENTRY_PT_SYMBOL)
  184. except:
  185. raise Exception("Missing app entry point! Must be `int main(void) { ... }` ")
  186. jump_table_address = get_symbol_addr(nm_output, JUMP_TABLE_ADDR_SYMBOL)
  187. reloc_entries = get_relocate_entries(target_elf)
  188. statinfo = os.stat(target_binary)
  189. app_load_size = statinfo.st_size
  190. if resources_file is not None:
  191. with open(resources_file, 'rb') as f:
  192. pbpack = ResourcePack.deserialize(f, is_system=False)
  193. resource_crc = pbpack.get_content_crc()
  194. else:
  195. resource_crc = 0
  196. if DEBUG:
  197. copy2(target_binary, target_binary + ".orig")
  198. with open(target_binary, 'r+b') as f:
  199. total_app_image_size = app_load_size + (len(reloc_entries) * 4)
  200. if total_app_image_size > MAX_APP_BINARY_SIZE:
  201. raise Exception("App image size is %u (app %u relocation table %u). Must be smaller "
  202. "than %u bytes" % (total_app_image_size,
  203. app_load_size,
  204. len(reloc_entries) * 4,
  205. MAX_APP_BINARY_SIZE))
  206. def read_value_at_offset(offset, format_str, size):
  207. f.seek(offset)
  208. return unpack(format_str, f.read(size))
  209. app_bin = f.read()
  210. app_crc = stm32_crc.crc32(app_bin[STRUCT_SIZE_BYTES:])
  211. [app_flags] = read_value_at_offset(FLAGS_ADDR, '<L', 4)
  212. if allow_js:
  213. app_flags = app_flags | PROCESS_INFO_ALLOW_JS
  214. if has_worker:
  215. app_flags = app_flags | PROCESS_INFO_HAS_WORKER
  216. app_virtual_size = get_virtual_size(target_elf)
  217. struct_changes = {
  218. 'load_size' : app_load_size,
  219. 'entry_point' : "0x%08x" % app_entry_address,
  220. 'symbol_table' : "0x%08x" % jump_table_address,
  221. 'flags' : app_flags,
  222. 'crc' : "0x%08x" % app_crc,
  223. 'num_reloc_entries': "0x%08x" % len(reloc_entries),
  224. 'resource_crc' : "0x%08x" % resource_crc,
  225. 'timestamp' : timestamp,
  226. 'virtual_size': app_virtual_size
  227. }
  228. def write_value_at_offset(offset, format_str, value):
  229. f.seek(offset)
  230. f.write(pack(format_str, value))
  231. write_value_at_offset(LOAD_SIZE_ADDR, '<H', app_load_size)
  232. write_value_at_offset(OFFSET_ADDR, '<L', app_entry_address)
  233. write_value_at_offset(CRC_ADDR, '<L', app_crc)
  234. write_value_at_offset(RESOURCE_CRC_ADDR, '<L', resource_crc)
  235. write_value_at_offset(RESOURCE_TIMESTAMP_ADDR, '<L', timestamp)
  236. write_value_at_offset(JUMP_TABLE_ADDR, '<L', jump_table_address)
  237. write_value_at_offset(FLAGS_ADDR, '<L', app_flags)
  238. write_value_at_offset(NUM_RELOC_ENTRIES_ADDR, '<L', len(reloc_entries))
  239. write_value_at_offset(VIRTUAL_SIZE_ADDR, "<H", app_virtual_size)
  240. # Write the reloc_entries past the end of the binary. This expands the size of the binary,
  241. # but this new stuff won't actually be loaded into ram.
  242. f.seek(app_load_size)
  243. for entry in reloc_entries:
  244. f.write(pack('<L', entry))
  245. f.flush()
  246. return struct_changes