#!/usr/bin/ruby # A script to statically list syscalls used by a given binary. # # Syntax: list-syscalls.rb [--only-used-syscalls] # # NOTE: For accurate results, build the binary with musl and LTO enabled. # Example: ./polkadot/scripts/list-syscalls/list-syscalls.rb target/x86_64-unknown-linux-musl/production/polkadot-prepare-worker --only-used-syscalls # # Author: @koute # Source: https://gist.github.com/koute/166f82bfee5e27324077891008fca6eb require 'shellwords' require 'set' SYNTAX_STRING = 'Syntax: list-syscalls.rb [--only-used-syscalls]'.freeze # Generated from `libc` using the following regex: # 'pub const SYS_([a-z0-9_]+): ::c_long = (\d+);' # ' \2 => "\1",' SYSCALLS = { 0 => 'read', 1 => 'write', 2 => 'open', 3 => 'close', 4 => 'stat', 5 => 'fstat', 6 => 'lstat', 7 => 'poll', 8 => 'lseek', 9 => 'mmap', 10 => 'mprotect', 11 => 'munmap', 12 => 'brk', 13 => 'rt_sigaction', 14 => 'rt_sigprocmask', 15 => 'rt_sigreturn', 16 => 'ioctl', 17 => 'pread64', 18 => 'pwrite64', 19 => 'readv', 20 => 'writev', 21 => 'access', 22 => 'pipe', 23 => 'select', 24 => 'sched_yield', 25 => 'mremap', 26 => 'msync', 27 => 'mincore', 28 => 'madvise', 29 => 'shmget', 30 => 'shmat', 31 => 'shmctl', 32 => 'dup', 33 => 'dup2', 34 => 'pause', 35 => 'nanosleep', 36 => 'getitimer', 37 => 'alarm', 38 => 'setitimer', 39 => 'getpid', 40 => 'sendfile', 41 => 'socket', 42 => 'connect', 43 => 'accept', 44 => 'sendto', 45 => 'recvfrom', 46 => 'sendmsg', 47 => 'recvmsg', 48 => 'shutdown', 49 => 'bind', 50 => 'listen', 51 => 'getsockname', 52 => 'getpeername', 53 => 'socketpair', 54 => 'setsockopt', 55 => 'getsockopt', 56 => 'clone', 57 => 'fork', 58 => 'vfork', 59 => 'execve', 60 => 'exit', 61 => 'wait4', 62 => 'kill', 63 => 'uname', 64 => 'semget', 65 => 'semop', 66 => 'semctl', 67 => 'shmdt', 68 => 'msgget', 69 => 'msgsnd', 70 => 'msgrcv', 71 => 'msgctl', 72 => 'fcntl', 73 => 'flock', 74 => 'fsync', 75 => 'fdatasync', 76 => 'truncate', 77 => 'ftruncate', 78 => 'getdents', 79 => 'getcwd', 80 => 'chdir', 81 => 'fchdir', 82 => 'rename', 83 => 'mkdir', 84 => 'rmdir', 85 => 'creat', 86 => 'link', 87 => 'unlink', 88 => 'symlink', 89 => 'readlink', 90 => 'chmod', 91 => 'fchmod', 92 => 'chown', 93 => 'fchown', 94 => 'lchown', 95 => 'umask', 96 => 'gettimeofday', 97 => 'getrlimit', 98 => 'getrusage', 99 => 'sysinfo', 100 => 'times', 101 => 'ptrace', 102 => 'getuid', 103 => 'syslog', 104 => 'getgid', 105 => 'setuid', 106 => 'setgid', 107 => 'geteuid', 108 => 'getegid', 109 => 'setpgid', 110 => 'getppid', 111 => 'getpgrp', 112 => 'setsid', 113 => 'setreuid', 114 => 'setregid', 115 => 'getgroups', 116 => 'setgroups', 117 => 'setresuid', 118 => 'getresuid', 119 => 'setresgid', 120 => 'getresgid', 121 => 'getpgid', 122 => 'setfsuid', 123 => 'setfsgid', 124 => 'getsid', 125 => 'capget', 126 => 'capset', 127 => 'rt_sigpending', 128 => 'rt_sigtimedwait', 129 => 'rt_sigqueueinfo', 130 => 'rt_sigsuspend', 131 => 'sigaltstack', 132 => 'utime', 133 => 'mknod', 134 => 'uselib', 135 => 'personality', 136 => 'ustat', 137 => 'statfs', 138 => 'fstatfs', 139 => 'sysfs', 140 => 'getpriority', 141 => 'setpriority', 142 => 'sched_setparam', 143 => 'sched_getparam', 144 => 'sched_setscheduler', 145 => 'sched_getscheduler', 146 => 'sched_get_priority_max', 147 => 'sched_get_priority_min', 148 => 'sched_rr_get_interval', 149 => 'mlock', 150 => 'munlock', 151 => 'mlockall', 152 => 'munlockall', 153 => 'vhangup', 154 => 'modify_ldt', 155 => 'pivot_root', 156 => '_sysctl', 157 => 'prctl', 158 => 'arch_prctl', 159 => 'adjtimex', 160 => 'setrlimit', 161 => 'chroot', 162 => 'sync', 163 => 'acct', 164 => 'settimeofday', 165 => 'mount', 166 => 'umount2', 167 => 'swapon', 168 => 'swapoff', 169 => 'reboot', 170 => 'sethostname', 171 => 'setdomainname', 172 => 'iopl', 173 => 'ioperm', 174 => 'create_module', 175 => 'init_module', 176 => 'delete_module', 177 => 'get_kernel_syms', 178 => 'query_module', 179 => 'quotactl', 180 => 'nfsservctl', 181 => 'getpmsg', 182 => 'putpmsg', 183 => 'afs_syscall', 184 => 'tuxcall', 185 => 'security', 186 => 'gettid', 187 => 'readahead', 188 => 'setxattr', 189 => 'lsetxattr', 190 => 'fsetxattr', 191 => 'getxattr', 192 => 'lgetxattr', 193 => 'fgetxattr', 194 => 'listxattr', 195 => 'llistxattr', 196 => 'flistxattr', 197 => 'removexattr', 198 => 'lremovexattr', 199 => 'fremovexattr', 200 => 'tkill', 201 => 'time', 202 => 'futex', 203 => 'sched_setaffinity', 204 => 'sched_getaffinity', 205 => 'set_thread_area', 206 => 'io_setup', 207 => 'io_destroy', 208 => 'io_getevents', 209 => 'io_submit', 210 => 'io_cancel', 211 => 'get_thread_area', 212 => 'lookup_dcookie', 213 => 'epoll_create', 214 => 'epoll_ctl_old', 215 => 'epoll_wait_old', 216 => 'remap_file_pages', 217 => 'getdents64', 218 => 'set_tid_address', 219 => 'restart_syscall', 220 => 'semtimedop', 221 => 'fadvise64', 222 => 'timer_create', 223 => 'timer_settime', 224 => 'timer_gettime', 225 => 'timer_getoverrun', 226 => 'timer_delete', 227 => 'clock_settime', 228 => 'clock_gettime', 229 => 'clock_getres', 230 => 'clock_nanosleep', 231 => 'exit_group', 232 => 'epoll_wait', 233 => 'epoll_ctl', 234 => 'tgkill', 235 => 'utimes', 236 => 'vserver', 237 => 'mbind', 238 => 'set_mempolicy', 239 => 'get_mempolicy', 240 => 'mq_open', 241 => 'mq_unlink', 242 => 'mq_timedsend', 243 => 'mq_timedreceive', 244 => 'mq_notify', 245 => 'mq_getsetattr', 246 => 'kexec_load', 247 => 'waitid', 248 => 'add_key', 249 => 'request_key', 250 => 'keyctl', 251 => 'ioprio_set', 252 => 'ioprio_get', 253 => 'inotify_init', 254 => 'inotify_add_watch', 255 => 'inotify_rm_watch', 256 => 'migrate_pages', 257 => 'openat', 258 => 'mkdirat', 259 => 'mknodat', 260 => 'fchownat', 261 => 'futimesat', 262 => 'newfstatat', 263 => 'unlinkat', 264 => 'renameat', 265 => 'linkat', 266 => 'symlinkat', 267 => 'readlinkat', 268 => 'fchmodat', 269 => 'faccessat', 270 => 'pselect6', 271 => 'ppoll', 272 => 'unshare', 273 => 'set_robust_list', 274 => 'get_robust_list', 275 => 'splice', 276 => 'tee', 277 => 'sync_file_range', 278 => 'vmsplice', 279 => 'move_pages', 280 => 'utimensat', 281 => 'epoll_pwait', 282 => 'signalfd', 283 => 'timerfd_create', 284 => 'eventfd', 285 => 'fallocate', 286 => 'timerfd_settime', 287 => 'timerfd_gettime', 288 => 'accept4', 289 => 'signalfd4', 290 => 'eventfd2', 291 => 'epoll_create1', 292 => 'dup3', 293 => 'pipe2', 294 => 'inotify_init1', 295 => 'preadv', 296 => 'pwritev', 297 => 'rt_tgsigqueueinfo', 298 => 'perf_event_open', 299 => 'recvmmsg', 300 => 'fanotify_init', 301 => 'fanotify_mark', 302 => 'prlimit64', 303 => 'name_to_handle_at', 304 => 'open_by_handle_at', 305 => 'clock_adjtime', 306 => 'syncfs', 307 => 'sendmmsg', 308 => 'setns', 309 => 'getcpu', 310 => 'process_vm_readv', 311 => 'process_vm_writev', 312 => 'kcmp', 313 => 'finit_module', 314 => 'sched_setattr', 315 => 'sched_getattr', 316 => 'renameat2', 317 => 'seccomp', 318 => 'getrandom', 319 => 'memfd_create', 320 => 'kexec_file_load', 321 => 'bpf', 322 => 'execveat', 323 => 'userfaultfd', 324 => 'membarrier', 325 => 'mlock2', 326 => 'copy_file_range', 327 => 'preadv2', 328 => 'pwritev2', 329 => 'pkey_mprotect', 330 => 'pkey_alloc', 331 => 'pkey_free', 332 => 'statx', 334 => 'rseq', 424 => 'pidfd_send_signal', 425 => 'io_uring_setup', 426 => 'io_uring_enter', 427 => 'io_uring_register', 428 => 'open_tree', 429 => 'move_mount', 430 => 'fsopen', 431 => 'fsconfig', 432 => 'fsmount', 433 => 'fspick', 434 => 'pidfd_open', 435 => 'clone3', 436 => 'close_range', 437 => 'openat2', 438 => 'pidfd_getfd', 439 => 'faccessat2', 440 => 'process_madvise', 441 => 'epoll_pwait2', 442 => 'mount_setattr', 443 => 'quotactl_fd', 444 => 'landlock_create_ruleset', 445 => 'landlock_add_rule', 446 => 'landlock_restrict_self', 447 => 'memfd_secret', 448 => 'process_mrelease', 449 => 'futex_waitv', 450 => 'set_mempolicy_home_node' }.map { |num, name| [num, "#{num} (#{name})"] }.to_h REGS_R64 = %w[ rax rbx rcx rdx rsi rdi rsp rbp r8 r9 r10 r11 r12 r13 r14 r15 ] REGS_R32 = %w[ eax ebx ecx edx esi edi esp ebp r8d r9d r10d r11d r12d r13d r14d r15d ] REGS_R16 = %w[ ax bx cx dx si di sp bp r8w r9w r10w r11w r12w r13w r14w r15w ] REGS_R8 = %w[ al bl cl dl sil dil spl bpl r8b r9b r10b r11b r12b r13b r14b r15b ] REG_MAP = (REGS_R64.map { |r| [r, r] } + REGS_R32.zip(REGS_R64) + REGS_R16.zip(REGS_R64) + REGS_R8.zip(REGS_R64)).to_h REGS_R = (REGS_R64 + REGS_R32 + REGS_R16 + REGS_R8).join('|') if ARGV.empty? warn SYNTAX_STRING exit 1 end file_path = ARGV[0] raise "no such file: #{file_path}" unless File.exist? file_path only_used_syscalls = false ARGV[1..].each do |arg| if arg == '--only-used-syscalls' only_used_syscalls = true else warn "invalid argument '#{arg}':\n#{SYNTAX_STRING}" exit 1 end end puts 'Running objdump...' unless only_used_syscalls dump = `objdump -wd -j .text -M intel #{file_path.shellescape}` raise 'objdump failed' unless $?.exitstatus == 0 puts 'Parsing objdump output...' unless only_used_syscalls current_fn = nil code_for_fn = {} fns_with_syscall = Set.new fns_with_indirect_syscall = Set.new dump.split("\n").each do |line| if line =~ /\A[0-9a-f]+ <(.+?)>:/ current_fn = Regexp.last_match(1) next end next unless current_fn next if %w[syscall __syscall_cp_c].include?(current_fn) # These are for indirect syscalls. code = line.strip.split("\t")[2] next if [nil, ''].include?(code) code_for_fn[current_fn] ||= [] code_for_fn[current_fn] << code.gsub(/[\t ]+/, ' ') fns_with_syscall.add(current_fn) if code == 'syscall' fns_with_indirect_syscall.add(current_fn) if code =~ /<(syscall|__syscall_cp)>/ end unless only_used_syscalls puts "Found #{fns_with_syscall.length} functions doing direct syscalls" puts "Found #{fns_with_indirect_syscall.length} functions doing indirect syscalls" end syscalls_for_fn = {} not_found_count = 0 (fns_with_syscall + fns_with_indirect_syscall).each do |fn_name| syscalls_for_fn[fn_name] ||= [] if fn_name =~ /_ZN11parking_lot9raw_mutex8RawMutex9lock_slow.+/ # Hardcode 'SYS_futex' as this function produces a really messy assembly. syscalls_for_fn[fn_name] << 202 next end code = code_for_fn[fn_name] found = false regs = {} code.each do |inst| if inst =~ /mov (#{REGS_R}),(.+)/ reg = Regexp.last_match(1) value = Regexp.last_match(2) regs[REG_MAP[reg]] = if value =~ /#{REGS_R}/ regs[REG_MAP[value]] elsif value =~ /0x([0-9a-f]+)/ Regexp.last_match(1).to_i(16) end elsif inst =~ /xor (#{REGS_R}),(#{REGS_R})/ reg_1 = Regexp.last_match(1) reg_2 = Regexp.last_match(1) regs[REG_MAP[reg_1]] = 0 if reg_1 == reg_2 elsif inst =~ /lea (#{REGS_R}),(.+)/ reg = Regexp.last_match(1) value = Regexp.last_match(2) regs[REG_MAP[reg]] = ('syscall' if value.strip =~ /\[rip\+0x[a-z0-9]+\]\s*#\s*[0-9a-f]+\s*/) elsif inst =~ /(call|jmp) (#{REGS_R})/ reg = Regexp.last_match(2) if regs[REG_MAP[reg]] == 'syscall' if !regs['rdi'].nil? syscalls_for_fn[fn_name] << regs['rdi'] found = true else found = false end end elsif inst =~ /(call|jmp) [0-9a-f]+ <(syscall|__syscall_cp)>/ if !regs['rdi'].nil? syscalls_for_fn[fn_name] << regs['rdi'] found = true else found = false end elsif inst == 'syscall' if !regs['rax'].nil? syscalls_for_fn[fn_name] << regs['rax'] found = true else found = false end end end next if found puts "WARN: Function triggers a syscall but couldn't figure out which one: #{fn_name}" puts ' ' + code.join("\n ") puts not_found_count += 1 end puts "WARN: Failed to figure out syscall for #{not_found_count} function(s)" if not_found_count > 0 fns_for_syscall = {} syscalls_for_fn.each do |fn_name, syscalls| syscalls.each do |syscall| fns_for_syscall[syscall] ||= [] fns_for_syscall[syscall] << fn_name end end if only_used_syscalls puts syscalls_for_fn.values.flatten.sort.uniq.map { |sc| SYSCALLS[sc] || sc }.join("\n") else puts 'Functions per syscall:' fns_for_syscall.sort_by { |sc, _| sc }.each do |syscall, fn_names| fn_names = fn_names.sort.uniq puts " #{SYSCALLS[syscall] || syscall} [#{fn_names.length} functions]" fn_names.each do |fn_name| puts " #{fn_name}" end end puts puts 'Used syscalls:' puts ' ' + syscalls_for_fn.values.flatten.sort.uniq.map { |sc| SYSCALLS[sc] || sc }.join("\n ") end