#!/usr/bin/env python # This tool is used to generate the assembler system call stubs, # the header files listing all available system calls, and the # makefiles used to build all the stubs. import atexit import commands import filecmp import glob import logging import os.path import re import shutil import stat import string import sys import tempfile all_arches = [ "arm", "arm64", "mips", "mips64", "x86", "x86_64" ] # temp directory where we store all intermediate files bionic_temp = tempfile.mkdtemp(prefix="bionic_gensyscalls"); # Make sure the directory is deleted when the script exits. atexit.register(shutil.rmtree, bionic_temp) bionic_libc_root = os.path.join(os.environ["ANDROID_BUILD_TOP"], "bionic/libc") warning = "Generated by gensyscalls.py. Do not edit." DRY_RUN = False def make_dir(path): path = os.path.abspath(path) if not os.path.exists(path): parent = os.path.dirname(path) if parent: make_dir(parent) os.mkdir(path) def create_file(relpath): full_path = os.path.join(bionic_temp, relpath) dir = os.path.dirname(full_path) make_dir(dir) return open(full_path, "w") syscall_stub_header = "/* " + warning + " */\n" + \ """ #include ENTRY(%(func)s) """ # # ARM assembler templates for each syscall stub # arm_eabi_call_default = syscall_stub_header + """\ mov ip, r7 .cfi_register r7, ip ldr r7, =%(__NR_name)s swi #0 mov r7, ip .cfi_restore r7 cmn r0, #(MAX_ERRNO + 1) bxls lr neg r0, r0 b __set_errno_internal END(%(func)s) """ arm_eabi_call_long = syscall_stub_header + """\ mov ip, sp stmfd sp!, {r4, r5, r6, r7} .cfi_def_cfa_offset 16 .cfi_rel_offset r4, 0 .cfi_rel_offset r5, 4 .cfi_rel_offset r6, 8 .cfi_rel_offset r7, 12 ldmfd ip, {r4, r5, r6} ldr r7, =%(__NR_name)s swi #0 ldmfd sp!, {r4, r5, r6, r7} .cfi_def_cfa_offset 0 cmn r0, #(MAX_ERRNO + 1) bxls lr neg r0, r0 b __set_errno_internal END(%(func)s) """ # # Arm64 assembler templates for each syscall stub # arm64_call = syscall_stub_header + """\ mov x8, %(__NR_name)s svc #0 cmn x0, #(MAX_ERRNO + 1) cneg x0, x0, hi b.hi __set_errno_internal ret END(%(func)s) """ # # MIPS assembler templates for each syscall stub # mips_call = syscall_stub_header + """\ .set noreorder .cpload $t9 li $v0, %(__NR_name)s syscall bnez $a3, 1f move $a0, $v0 j $ra nop 1: la $t9,__set_errno_internal j $t9 nop .set reorder END(%(func)s) """ # # MIPS64 assembler templates for each syscall stub # mips64_call = syscall_stub_header + """\ .set push .set noreorder li $v0, %(__NR_name)s syscall bnez $a3, 1f move $a0, $v0 j $ra nop 1: move $t0, $ra bal 2f nop 2: .cpsetup $ra, $t1, 2b LA $t9, __set_errno_internal .cpreturn j $t9 move $ra, $t0 .set pop END(%(func)s) """ # # x86 assembler templates for each syscall stub # x86_registers = [ "ebx", "ecx", "edx", "esi", "edi", "ebp" ] x86_call_prepare = """\ call __kernel_syscall pushl %eax .cfi_adjust_cfa_offset 4 .cfi_rel_offset eax, 0 """ x86_call = """\ movl $%(__NR_name)s, %%eax call *(%%esp) addl $4, %%esp cmpl $-MAX_ERRNO, %%eax jb 1f negl %%eax pushl %%eax call __set_errno_internal addl $4, %%esp 1: """ x86_return = """\ ret END(%(func)s) """ # # x86_64 assembler templates for each syscall stub # x86_64_call = """\ movl $%(__NR_name)s, %%eax syscall cmpq $-MAX_ERRNO, %%rax jb 1f negl %%eax movl %%eax, %%edi call __set_errno_internal 1: ret END(%(func)s) """ def param_uses_64bits(param): """Returns True iff a syscall parameter description corresponds to a 64-bit type.""" param = param.strip() # First, check that the param type begins with one of the known # 64-bit types. if not ( \ param.startswith("int64_t") or param.startswith("uint64_t") or \ param.startswith("loff_t") or param.startswith("off64_t") or \ param.startswith("long long") or param.startswith("unsigned long long") or param.startswith("signed long long") ): return False # Second, check that there is no pointer type here if param.find("*") >= 0: return False # Ok return True def count_arm_param_registers(params): """This function is used to count the number of register used to pass parameters when invoking an ARM system call. This is because the ARM EABI mandates that 64-bit quantities must be passed in an even+odd register pair. So, for example, something like: foo(int fd, off64_t pos) would actually need 4 registers: r0 -> int r1 -> unused r2-r3 -> pos """ count = 0 for param in params: if param_uses_64bits(param): if (count & 1) != 0: count += 1 count += 2 else: count += 1 return count def count_generic_param_registers(params): count = 0 for param in params: if param_uses_64bits(param): count += 2 else: count += 1 return count def count_generic_param_registers64(params): count = 0 for param in params: count += 1 return count # This lets us support regular system calls like __NR_write and also weird # ones like __ARM_NR_cacheflush, where the NR doesn't come at the start. def make__NR_name(name): if name.startswith("__ARM_NR_"): return name else: return "__NR_%s" % (name) def add_footer(pointer_length, stub, syscall): # Add any aliases for this syscall. aliases = syscall["aliases"] for alias in aliases: stub += "\nALIAS_SYMBOL(%s, %s)\n" % (alias, syscall["func"]) # Use hidden visibility on LP64 for any functions beginning with underscores. # Force hidden visibility for any functions which begin with 3 underscores if (pointer_length == 64 and syscall["func"].startswith("__")) or syscall["func"].startswith("___"): stub += '.hidden ' + syscall["func"] + '\n' return stub def arm_eabi_genstub(syscall): num_regs = count_arm_param_registers(syscall["params"]) if num_regs > 4: return arm_eabi_call_long % syscall return arm_eabi_call_default % syscall def arm64_genstub(syscall): return arm64_call % syscall def mips_genstub(syscall): return mips_call % syscall def mips64_genstub(syscall): return mips64_call % syscall def x86_genstub(syscall): result = syscall_stub_header % syscall numparams = count_generic_param_registers(syscall["params"]) stack_bias = numparams*4 + 8 offset = 0 mov_result = "" first_push = True for register in x86_registers[:numparams]: result += " pushl %%%s\n" % register if first_push: result += " .cfi_def_cfa_offset 8\n" result += " .cfi_rel_offset %s, 0\n" % register first_push = False else: result += " .cfi_adjust_cfa_offset 4\n" result += " .cfi_rel_offset %s, 0\n" % register mov_result += " mov %d(%%esp), %%%s\n" % (stack_bias+offset, register) offset += 4 result += x86_call_prepare result += mov_result result += x86_call % syscall for register in reversed(x86_registers[:numparams]): result += " popl %%%s\n" % register result += x86_return % syscall return result def x86_genstub_socketcall(syscall): # %ebx <--- Argument 1 - The call id of the needed vectored # syscall (socket, bind, recv, etc) # %ecx <--- Argument 2 - Pointer to the rest of the arguments # from the original function called (socket()) result = syscall_stub_header % syscall # save the regs we need result += " pushl %ebx\n" result += " .cfi_def_cfa_offset 8\n" result += " .cfi_rel_offset ebx, 0\n" result += " pushl %ecx\n" result += " .cfi_adjust_cfa_offset 4\n" result += " .cfi_rel_offset ecx, 0\n" stack_bias = 16 result += x86_call_prepare # set the call id (%ebx) result += " mov $%d, %%ebx\n" % syscall["socketcall_id"] # set the pointer to the rest of the args into %ecx result += " mov %esp, %ecx\n" result += " addl $%d, %%ecx\n" % (stack_bias) # now do the syscall code itself result += x86_call % syscall # now restore the saved regs result += " popl %ecx\n" result += " popl %ebx\n" # epilog result += x86_return % syscall return result def x86_64_genstub(syscall): result = syscall_stub_header % syscall num_regs = count_generic_param_registers64(syscall["params"]) if (num_regs > 3): # rcx is used as 4th argument. Kernel wants it at r10. result += " movq %rcx, %r10\n" result += x86_64_call % syscall return result class SysCallsTxtParser: def __init__(self): self.syscalls = [] self.lineno = 0 def E(self, msg): print "%d: %s" % (self.lineno, msg) def parse_line(self, line): """ parse a syscall spec line. line processing, format is return type func_name[|alias_list][:syscall_name[:socketcall_id]] ( [paramlist] ) architecture_list """ pos_lparen = line.find('(') E = self.E if pos_lparen < 0: E("missing left parenthesis in '%s'" % line) return pos_rparen = line.rfind(')') if pos_rparen < 0 or pos_rparen <= pos_lparen: E("missing or misplaced right parenthesis in '%s'" % line) return return_type = line[:pos_lparen].strip().split() if len(return_type) < 2: E("missing return type in '%s'" % line) return syscall_func = return_type[-1] return_type = string.join(return_type[:-1],' ') socketcall_id = -1 pos_colon = syscall_func.find(':') if pos_colon < 0: syscall_name = syscall_func else: if pos_colon == 0 or pos_colon+1 >= len(syscall_func): E("misplaced colon in '%s'" % line) return # now find if there is a socketcall_id for a dispatch-type syscall # after the optional 2nd colon pos_colon2 = syscall_func.find(':', pos_colon + 1) if pos_colon2 < 0: syscall_name = syscall_func[pos_colon+1:] syscall_func = syscall_func[:pos_colon] else: if pos_colon2+1 >= len(syscall_func): E("misplaced colon2 in '%s'" % line) return syscall_name = syscall_func[(pos_colon+1):pos_colon2] socketcall_id = int(syscall_func[pos_colon2+1:]) syscall_func = syscall_func[:pos_colon] alias_delim = syscall_func.find('|') if alias_delim > 0: alias_list = syscall_func[alias_delim+1:].strip() syscall_func = syscall_func[:alias_delim] alias_delim = syscall_name.find('|') if alias_delim > 0: syscall_name = syscall_name[:alias_delim] syscall_aliases = string.split(alias_list, ',') else: syscall_aliases = [] if pos_rparen > pos_lparen+1: syscall_params = line[pos_lparen+1:pos_rparen].split(',') params = string.join(syscall_params,',') else: syscall_params = [] params = "void" t = { "name" : syscall_name, "func" : syscall_func, "aliases" : syscall_aliases, "params" : syscall_params, "decl" : "%-15s %s (%s);" % (return_type, syscall_func, params), "socketcall_id" : socketcall_id } # Parse the architecture list. arch_list = line[pos_rparen+1:].strip() if arch_list == "all": for arch in all_arches: t[arch] = True else: for arch in string.split(arch_list, ','): if arch in all_arches: t[arch] = True else: E("invalid syscall architecture '%s' in '%s'" % (arch, line)) return self.syscalls.append(t) logging.debug(t) def parse_open_file(self, fp): for line in fp: self.lineno += 1 line = line.strip() if not line: continue if line[0] == '#': continue self.parse_line(line) def parse_file(self, file_path): logging.debug("parse_file: %s" % file_path) with open(file_path) as fp: self.parse_open_file(fp) class State: def __init__(self): self.old_stubs = [] self.new_stubs = [] self.other_files = [] self.syscalls = [] def process_file(self, input): parser = SysCallsTxtParser() parser.parse_file(input) self.syscalls = parser.syscalls parser = None for syscall in self.syscalls: syscall["__NR_name"] = make__NR_name(syscall["name"]) if syscall.has_key("arm"): syscall["asm-arm"] = add_footer(32, arm_eabi_genstub(syscall), syscall) if syscall.has_key("arm64"): syscall["asm-arm64"] = add_footer(64, arm64_genstub(syscall), syscall) if syscall.has_key("x86"): if syscall["socketcall_id"] >= 0: syscall["asm-x86"] = add_footer(32, x86_genstub_socketcall(syscall), syscall) else: syscall["asm-x86"] = add_footer(32, x86_genstub(syscall), syscall) elif syscall["socketcall_id"] >= 0: E("socketcall_id for dispatch syscalls is only supported for x86 in '%s'" % t) return if syscall.has_key("mips"): syscall["asm-mips"] = add_footer(32, mips_genstub(syscall), syscall) if syscall.has_key("mips64"): syscall["asm-mips64"] = add_footer(64, mips64_genstub(syscall), syscall) if syscall.has_key("x86_64"): syscall["asm-x86_64"] = add_footer(64, x86_64_genstub(syscall), syscall) # Scan Linux kernel asm/unistd.h files containing __NR_* constants # and write out equivalent SYS_* constants for glibc source compatibility. def gen_glibc_syscalls_h(self): glibc_syscalls_h_path = "include/bits/glibc-syscalls.h" logging.info("generating " + glibc_syscalls_h_path) glibc_fp = create_file(glibc_syscalls_h_path) glibc_fp.write("/* %s */\n" % warning) glibc_fp.write("#ifndef _BIONIC_BITS_GLIBC_SYSCALLS_H_\n") glibc_fp.write("#define _BIONIC_BITS_GLIBC_SYSCALLS_H_\n") # Collect the set of all syscalls for all architectures. syscalls = set() pattern = re.compile(r'^\s*#\s*define\s*__NR_([a-z_]\S+)') for unistd_h in ["kernel/uapi/asm-generic/unistd.h", "kernel/uapi/asm-arm/asm/unistd.h", "kernel/uapi/asm-arm/asm/unistd-common.h", "kernel/uapi/asm-arm/asm/unistd-eabi.h", "kernel/uapi/asm-arm/asm/unistd-oabi.h", "kernel/uapi/asm-mips/asm/unistd.h", "kernel/uapi/asm-x86/asm/unistd_32.h", "kernel/uapi/asm-x86/asm/unistd_64.h"]: for line in open(os.path.join(bionic_libc_root, unistd_h)): m = re.search(pattern, line) if m: nr_name = m.group(1) if 'reserved' not in nr_name and 'unused' not in nr_name: syscalls.add(nr_name) # Write out a single file listing them all. Note that the input # files include #if trickery, so even for a single architecture # we don't know exactly which ones are available. # https://code.google.com/p/android/issues/detail?id=215853 for syscall in sorted(syscalls): nr_name = make__NR_name(syscall) glibc_fp.write("#if defined(%s)\n" % nr_name) glibc_fp.write(" #define SYS_%s %s\n" % (syscall, nr_name)) glibc_fp.write("#endif\n") glibc_fp.write("#endif /* _BIONIC_BITS_GLIBC_SYSCALLS_H_ */\n") glibc_fp.close() self.other_files.append(glibc_syscalls_h_path) # Write each syscall stub. def gen_syscall_stubs(self): for syscall in self.syscalls: for arch in all_arches: if syscall.has_key("asm-%s" % arch): filename = "arch-%s/syscalls/%s.S" % (arch, syscall["func"]) logging.info(">>> generating " + filename) fp = create_file(filename) fp.write(syscall["asm-%s" % arch]) fp.close() self.new_stubs.append(filename) def regenerate(self): logging.info("scanning for existing architecture-specific stub files...") for arch in all_arches: arch_dir = "arch-" + arch logging.info("scanning " + os.path.join(bionic_libc_root, arch_dir)) rel_path = os.path.join(arch_dir, "syscalls") for file in os.listdir(os.path.join(bionic_libc_root, rel_path)): if file.endswith(".S"): self.old_stubs.append(os.path.join(rel_path, file)) logging.info("found %d stub files" % len(self.old_stubs)) if not os.path.exists(bionic_temp): logging.info("creating %s..." % bionic_temp) make_dir(bionic_temp) logging.info("re-generating stubs and support files...") self.gen_glibc_syscalls_h() self.gen_syscall_stubs() logging.info("comparing files...") adds = [] edits = [] for stub in self.new_stubs + self.other_files: tmp_file = os.path.join(bionic_temp, stub) libc_file = os.path.join(bionic_libc_root, stub) if not os.path.exists(libc_file): # new file, git add it logging.info("new file: " + stub) adds.append(libc_file) shutil.copyfile(tmp_file, libc_file) elif not filecmp.cmp(tmp_file, libc_file): logging.info("changed file: " + stub) edits.append(stub) deletes = [] for stub in self.old_stubs: if not stub in self.new_stubs: logging.info("deleted file: " + stub) deletes.append(os.path.join(bionic_libc_root, stub)) if not DRY_RUN: if adds: commands.getoutput("git add " + " ".join(adds)) if deletes: commands.getoutput("git rm " + " ".join(deletes)) if edits: for file in edits: shutil.copyfile(os.path.join(bionic_temp, file), os.path.join(bionic_libc_root, file)) commands.getoutput("git add " + " ".join((os.path.join(bionic_libc_root, file)) for file in edits)) commands.getoutput("git add %s" % (os.path.join(bionic_libc_root, "SYSCALLS.TXT"))) if (not adds) and (not deletes) and (not edits): logging.info("no changes detected!") else: logging.info("ready to go!!") logging.basicConfig(level=logging.INFO) if __name__ == "__main__": state = State() state.process_file(os.path.join(bionic_libc_root, "SYSCALLS.TXT")) state.regenerate()