1#!/usr/bin/env python3 2 3import argparse 4import os 5import platform 6import subprocess 7 8# This list contains symbols that _might_ be exported for some platforms 9PLATFORM_SYMBOLS = [ 10 '_GLOBAL_OFFSET_TABLE_', 11 '__bss_end__', 12 '__bss_start__', 13 '__bss_start', 14 '__cxa_guard_abort', 15 '__cxa_guard_acquire', 16 '__cxa_guard_release', 17 '__cxa_allocate_dependent_exception', 18 '__cxa_allocate_exception', 19 '__cxa_begin_catch', 20 '__cxa_call_unexpected', 21 '__cxa_current_exception_type', 22 '__cxa_current_primary_exception', 23 '__cxa_decrement_exception_refcount', 24 '__cxa_deleted_virtual', 25 '__cxa_demangle', 26 '__cxa_end_catch', 27 '__cxa_free_dependent_exception', 28 '__cxa_free_exception', 29 '__cxa_get_exception_ptr', 30 '__cxa_get_globals', 31 '__cxa_get_globals_fast', 32 '__cxa_increment_exception_refcount', 33 '__cxa_new_handler', 34 '__cxa_pure_virtual', 35 '__cxa_rethrow', 36 '__cxa_rethrow_primary_exception', 37 '__cxa_terminate_handler', 38 '__cxa_throw', 39 '__cxa_uncaught_exception', 40 '__cxa_uncaught_exceptions', 41 '__cxa_unexpected_handler', 42 '__dynamic_cast', 43 '__emutls_get_address', 44 '__gxx_personality_v0', 45 '__end__', 46 '__odr_asan._glapi_Context', 47 '__odr_asan._glapi_Dispatch', 48 '_bss_end__', 49 '_edata', 50 '_end', 51 '_fini', 52 '_init', 53 '_fbss', 54 '_fdata', 55 '_ftext', 56] 57 58def get_symbols_nm(nm, lib): 59 ''' 60 List all the (non platform-specific) symbols exported by the library 61 using `nm` 62 ''' 63 symbols = [] 64 platform_name = platform.system() 65 output = subprocess.check_output([nm, '-gP', lib], 66 stderr=open(os.devnull, 'w')).decode("ascii") 67 for line in output.splitlines(): 68 fields = line.split() 69 if len(fields) == 2 or fields[1] == 'U': 70 continue 71 symbol_name = fields[0] 72 if platform_name == 'Linux' or platform_name == 'GNU' or platform_name.startswith('GNU/'): 73 if symbol_name in PLATFORM_SYMBOLS: 74 continue 75 elif platform_name == 'Darwin': 76 assert symbol_name[0] == '_' 77 symbol_name = symbol_name[1:] 78 symbols.append(symbol_name) 79 return symbols 80 81 82def get_symbols_dumpbin(dumpbin, lib): 83 ''' 84 List all the (non platform-specific) symbols exported by the library 85 using `dumpbin` 86 ''' 87 symbols = [] 88 output = subprocess.check_output([dumpbin, '/exports', lib], 89 stderr=open(os.devnull, 'w')).decode("ascii") 90 for line in output.splitlines(): 91 fields = line.split() 92 # The lines with the symbols are made of at least 4 columns; see details below 93 if len(fields) < 4: 94 continue 95 try: 96 # Making sure the first 3 columns are a dec counter, a hex counter 97 # and a hex address 98 _ = int(fields[0], 10) 99 _ = int(fields[1], 16) 100 _ = int(fields[2], 16) 101 except ValueError: 102 continue 103 symbol_name = fields[3] 104 # De-mangle symbols 105 if symbol_name[0] == '_' and '@' in symbol_name: 106 symbol_name = symbol_name[1:].split('@')[0] 107 symbols.append(symbol_name) 108 return symbols 109 110 111def main(): 112 parser = argparse.ArgumentParser() 113 parser.add_argument('--symbols-file', 114 action='store', 115 required=True, 116 help='path to file containing symbols') 117 parser.add_argument('--lib', 118 action='store', 119 required=True, 120 help='path to library') 121 parser.add_argument('--nm', 122 action='store', 123 help='path to binary (or name in $PATH)') 124 parser.add_argument('--dumpbin', 125 action='store', 126 help='path to binary (or name in $PATH)') 127 parser.add_argument('--ignore-symbol', 128 action='append', 129 help='do not process this symbol') 130 args = parser.parse_args() 131 132 try: 133 if platform.system() == 'Windows': 134 if not args.dumpbin: 135 parser.error('--dumpbin is mandatory') 136 lib_symbols = get_symbols_dumpbin(args.dumpbin, args.lib) 137 else: 138 if not args.nm: 139 parser.error('--nm is mandatory') 140 lib_symbols = get_symbols_nm(args.nm, args.lib) 141 except: 142 # We can't run this test, but we haven't technically failed it either 143 # Return the GNU "skip" error code 144 exit(77) 145 mandatory_symbols = [] 146 optional_symbols = [] 147 with open(args.symbols_file) as symbols_file: 148 qualifier_optional = '(optional)' 149 for line in symbols_file.readlines(): 150 151 # Strip comments 152 line = line.split('#')[0] 153 line = line.strip() 154 if not line: 155 continue 156 157 # Line format: 158 # [qualifier] symbol 159 qualifier = None 160 symbol = None 161 162 fields = line.split() 163 if len(fields) == 1: 164 symbol = fields[0] 165 elif len(fields) == 2: 166 qualifier = fields[0] 167 symbol = fields[1] 168 else: 169 print(args.symbols_file + ': invalid format: ' + line) 170 exit(1) 171 172 # The only supported qualifier is 'optional', which means the 173 # symbol doesn't have to be exported by the library 174 if qualifier and not qualifier == qualifier_optional: 175 print(args.symbols_file + ': invalid qualifier: ' + qualifier) 176 exit(1) 177 178 if qualifier == qualifier_optional: 179 optional_symbols.append(symbol) 180 else: 181 mandatory_symbols.append(symbol) 182 183 unknown_symbols = [] 184 for symbol in lib_symbols: 185 if symbol in mandatory_symbols: 186 continue 187 if symbol in optional_symbols: 188 continue 189 if args.ignore_symbol and symbol in args.ignore_symbol: 190 continue 191 if symbol[:2] == '_Z': 192 # As ajax found out, the compiler intentionally exports symbols 193 # that we explicitly asked it not to export, and we can't do 194 # anything about it: 195 # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=36022#c4 196 continue 197 unknown_symbols.append(symbol) 198 199 missing_symbols = [ 200 sym for sym in mandatory_symbols if sym not in lib_symbols 201 ] 202 203 for symbol in unknown_symbols: 204 print(args.lib + ': unknown symbol exported: ' + symbol) 205 206 for symbol in missing_symbols: 207 print(args.lib + ': missing symbol: ' + symbol) 208 209 if unknown_symbols or missing_symbols: 210 exit(1) 211 exit(0) 212 213 214if __name__ == '__main__': 215 main() 216