diff options
-rw-r--r-- | runtime/arch/arm/jni_entrypoints_arm.S | 2 | ||||
-rw-r--r-- | runtime/arch/arm/quick_entrypoints_arm.S | 16 | ||||
-rw-r--r-- | runtime/arch/arm64/quick_entrypoints_arm64.S | 6 | ||||
-rw-r--r-- | runtime/interpreter/mterp/arm64ng/control_flow.S | 1 | ||||
-rw-r--r-- | runtime/interpreter/mterp/arm64ng/main.S | 2 | ||||
-rw-r--r-- | runtime/interpreter/mterp/armng/control_flow.S | 1 | ||||
-rw-r--r-- | runtime/interpreter/mterp/armng/main.S | 13 | ||||
-rwxr-xr-x | tools/check_cfi.py | 247 |
8 files changed, 282 insertions, 6 deletions
diff --git a/runtime/arch/arm/jni_entrypoints_arm.S b/runtime/arch/arm/jni_entrypoints_arm.S index 96b6241a20..fc57df76d3 100644 --- a/runtime/arch/arm/jni_entrypoints_arm.S +++ b/runtime/arch/arm/jni_entrypoints_arm.S @@ -114,11 +114,13 @@ ENTRY art_jni_dlsym_lookup_stub add sp, #12 @ restore stack pointer .cfi_adjust_cfa_offset -12 cbz r0, 1f @ is method code null? + .cfi_remember_state pop {r0, r1, r2, r3, lr} @ restore regs .cfi_adjust_cfa_offset -20 .cfi_restore lr bx r12 @ if non-null, tail call to method's code 1: + CFI_RESTORE_STATE_AND_DEF_CFA sp, 20 pop {r0, r1, r2, r3, pc} @ restore regs and return to caller to handle exception END art_jni_dlsym_lookup_stub diff --git a/runtime/arch/arm/quick_entrypoints_arm.S b/runtime/arch/arm/quick_entrypoints_arm.S index 7e11d32656..d6f129be50 100644 --- a/runtime/arch/arm/quick_entrypoints_arm.S +++ b/runtime/arch/arm/quick_entrypoints_arm.S @@ -686,6 +686,7 @@ ENTRY art_quick_aput_obj mov r0, r3 bl artIsAssignableFromCode cbz r0, .Lthrow_array_store_exception + .cfi_remember_state pop {r0-r2, lr} .cfi_restore r0 .cfi_restore r1 @@ -700,8 +701,13 @@ ENTRY art_quick_aput_obj strb r3, [r3, r0] blx lr .Lthrow_array_store_exception: + CFI_RESTORE_STATE_AND_DEF_CFA sp, 16 pop {r0-r2, lr} - /* No need to repeat restore cfi directives, the ones above apply here. */ + .cfi_restore r0 + .cfi_restore r1 + .cfi_restore r2 + .cfi_restore lr + .cfi_adjust_cfa_offset -16 SETUP_SAVE_ALL_CALLEE_SAVES_FRAME r3 mov r1, r2 mov r2, rSELF @ pass Thread::Current @@ -780,7 +786,7 @@ ENTRY \name RESTORE_SAVE_EVERYTHING_FRAME_KEEP_R0 REFRESH_MARKING_REGISTER bx lr - .cfi_restore_state + CFI_RESTORE_STATE_AND_DEF_CFA sp, FRAME_SIZE_SAVE_EVERYTHING 1: DELIVER_PENDING_EXCEPTION_FRAME_READY END \name @@ -1356,12 +1362,14 @@ ENTRY art_quick_resolution_trampoline mov r3, sp @ pass SP blx artQuickResolutionTrampoline @ (Method* called, receiver, Thread*, SP) cbz r0, 1f @ is code pointer null? goto exception + .cfi_remember_state mov r12, r0 ldr r0, [sp, #0] @ load resolved method in r0 RESTORE_SAVE_REFS_AND_ARGS_FRAME REFRESH_MARKING_REGISTER bx r12 @ tail-call into actual code 1: + CFI_RESTORE_STATE_AND_DEF_CFA sp, FRAME_SIZE_SAVE_REFS_AND_ARGS RESTORE_SAVE_REFS_AND_ARGS_FRAME DELIVER_PENDING_EXCEPTION END art_quick_resolution_trampoline @@ -1502,6 +1510,7 @@ ENTRY art_quick_instrumentation_entry mov r3, sp @ pass SP blx artInstrumentationMethodEntryFromCode @ (Method*, Object*, Thread*, SP) cbz r0, .Ldeliver_instrumentation_entry_exception + .cfi_remember_state @ Deliver exception if we got nullptr as function. mov r12, r0 @ r12 holds reference to code ldr r0, [sp, #4] @ restore r0 @@ -1511,6 +1520,7 @@ ENTRY art_quick_instrumentation_entry REFRESH_MARKING_REGISTER bx r12 @ call method with lr set to art_quick_instrumentation_exit .Ldeliver_instrumentation_entry_exception: + CFI_RESTORE_STATE_AND_DEF_CFA sp, FRAME_SIZE_SAVE_REFS_AND_ARGS @ Deliver exception for art_quick_instrumentation_entry placed after @ art_quick_instrumentation_exit so that the fallthrough works. RESTORE_SAVE_REFS_AND_ARGS_FRAME @@ -1529,6 +1539,7 @@ ENTRY art_quick_instrumentation_exit cbz r0, .Ldo_deliver_instrumentation_exception @ Deliver exception if we got nullptr as function. + .cfi_remember_state cbnz r1, .Ldeoptimize // Normal return. str r0, [sp, #FRAME_SIZE_SAVE_EVERYTHING - 4] @@ -1537,6 +1548,7 @@ ENTRY art_quick_instrumentation_exit REFRESH_MARKING_REGISTER bx lr .Ldo_deliver_instrumentation_exception: + CFI_RESTORE_STATE_AND_DEF_CFA sp, FRAME_SIZE_SAVE_EVERYTHING DELIVER_PENDING_EXCEPTION_FRAME_READY .Ldeoptimize: str r1, [sp, #FRAME_SIZE_SAVE_EVERYTHING - 4] diff --git a/runtime/arch/arm64/quick_entrypoints_arm64.S b/runtime/arch/arm64/quick_entrypoints_arm64.S index a20d558240..d8c91e11b9 100644 --- a/runtime/arch/arm64/quick_entrypoints_arm64.S +++ b/runtime/arch/arm64/quick_entrypoints_arm64.S @@ -1645,11 +1645,13 @@ ENTRY art_quick_proxy_invoke_handler bl artQuickProxyInvokeHandler // (Method* proxy method, receiver, Thread*, SP) ldr x2, [xSELF, THREAD_EXCEPTION_OFFSET] cbnz x2, .Lexception_in_proxy // success if no exception is pending + .cfi_remember_state RESTORE_SAVE_REFS_AND_ARGS_FRAME // Restore frame REFRESH_MARKING_REGISTER fmov d0, x0 // Store result in d0 in case it was float or double ret // return on success .Lexception_in_proxy: + CFI_RESTORE_STATE_AND_DEF_CFA sp, FRAME_SIZE_SAVE_REFS_AND_ARGS RESTORE_SAVE_REFS_AND_ARGS_FRAME DELIVER_PENDING_EXCEPTION END art_quick_proxy_invoke_handler @@ -1692,12 +1694,14 @@ ENTRY art_quick_resolution_trampoline mov x3, sp bl artQuickResolutionTrampoline // (called, receiver, Thread*, SP) cbz x0, 1f + .cfi_remember_state mov xIP0, x0 // Remember returned code pointer in xIP0. ldr x0, [sp, #0] // artQuickResolutionTrampoline puts called method in *SP. RESTORE_SAVE_REFS_AND_ARGS_FRAME REFRESH_MARKING_REGISTER br xIP0 1: + CFI_RESTORE_STATE_AND_DEF_CFA sp, FRAME_SIZE_SAVE_REFS_AND_ARGS RESTORE_SAVE_REFS_AND_ARGS_FRAME DELIVER_PENDING_EXCEPTION END art_quick_resolution_trampoline @@ -1924,6 +1928,7 @@ ENTRY art_quick_instrumentation_exit bl artInstrumentationMethodExitFromCode // (Thread*, SP, gpr_res*, fpr_res*) cbz x0, .Ldo_deliver_instrumentation_exception + .cfi_remember_state // Handle error cbnz x1, .Ldeoptimize // Normal return. @@ -1933,6 +1938,7 @@ ENTRY art_quick_instrumentation_exit REFRESH_MARKING_REGISTER br lr .Ldo_deliver_instrumentation_exception: + CFI_RESTORE_STATE_AND_DEF_CFA sp, FRAME_SIZE_SAVE_EVERYTHING DELIVER_PENDING_EXCEPTION_FRAME_READY .Ldeoptimize: str x1, [sp, #FRAME_SIZE_SAVE_EVERYTHING - 8] diff --git a/runtime/interpreter/mterp/arm64ng/control_flow.S b/runtime/interpreter/mterp/arm64ng/control_flow.S index f2d0559064..7873ca6b7c 100644 --- a/runtime/interpreter/mterp/arm64ng/control_flow.S +++ b/runtime/interpreter/mterp/arm64ng/control_flow.S @@ -168,6 +168,7 @@ RESTORE_ALL_CALLEE_SAVES ret .cfi_restore_state + CFI_DEF_CFA_BREG_PLUS_UCONST CFI_REFS, -8, CALLEE_SAVES_SIZE %def op_return_object(): % op_return(is_object="1", is_void="0", is_wide="0") diff --git a/runtime/interpreter/mterp/arm64ng/main.S b/runtime/interpreter/mterp/arm64ng/main.S index 8ada63c03e..89de81f5e4 100644 --- a/runtime/interpreter/mterp/arm64ng/main.S +++ b/runtime/interpreter/mterp/arm64ng/main.S @@ -1917,6 +1917,8 @@ artNterpAsmInstructionStart = .L_op_nop % return "nterp_" %def opcode_start(): NAME_START nterp_${opcode} + # Explicitly restore CFA, just in case the previous opcode clobbered it (by .cfi_def_*). + CFI_DEF_CFA_BREG_PLUS_UCONST CFI_REFS, -8, CALLEE_SAVES_SIZE %def opcode_end(): NAME_END nterp_${opcode} // Advance to the end of this handler. Causes error if we are past that point. diff --git a/runtime/interpreter/mterp/armng/control_flow.S b/runtime/interpreter/mterp/armng/control_flow.S index ab05228c2c..689b245729 100644 --- a/runtime/interpreter/mterp/armng/control_flow.S +++ b/runtime/interpreter/mterp/armng/control_flow.S @@ -168,6 +168,7 @@ .cfi_def_cfa sp, CALLEE_SAVES_SIZE RESTORE_ALL_CALLEE_SAVES lr_to_pc=1 .cfi_restore_state + CFI_DEF_CFA_BREG_PLUS_UCONST CFI_REFS, -4, CALLEE_SAVES_SIZE %def op_return_object(): % op_return(is_object="1", is_void="0", is_wide="0") diff --git a/runtime/interpreter/mterp/armng/main.S b/runtime/interpreter/mterp/armng/main.S index 5a086f597d..310a3fd8f1 100644 --- a/runtime/interpreter/mterp/armng/main.S +++ b/runtime/interpreter/mterp/armng/main.S @@ -488,6 +488,7 @@ END \name .cfi_adjust_cfa_offset -4 .if \lr_to_pc pop {r9-r11, pc} @ 9 words of callee saves and args. + .cfi_adjust_cfa_offset -16 .else pop {r9-r11, lr} @ 9 words of callee saves and args. .cfi_adjust_cfa_offset -16 @@ -1908,7 +1909,7 @@ EndExecuteNterpImpl: * expected common case is a "reasonable" value that converts directly * to modest integer. The EABI convert function isn't doing this for us. */ -nterp_d2l_doconv: +ENTRY nterp_d2l_doconv ubfx r2, r1, #20, #11 @ grab the exponent movw r3, #0x43e cmp r2, r3 @ MINLONG < x > MAXLONG? @@ -1931,6 +1932,7 @@ d2l_maybeNaN: mov r0, #0 mov r1, #0 bx lr @ return 0 for NaN +END nterp_d2l_doconv /* * Convert the float in r0 to a long in r0/r1. @@ -1939,7 +1941,7 @@ d2l_maybeNaN: * expected common case is a "reasonable" value that converts directly * to modest integer. The EABI convert function isn't doing this for us. */ -nterp_f2l_doconv: +ENTRY nterp_f2l_doconv ubfx r2, r0, #23, #8 @ grab the exponent cmp r2, #0xbe @ MININT < x > MAXINT? bhs f2l_special_cases @@ -1960,6 +1962,7 @@ f2l_maybeNaN: mov r0, #0 mov r1, #0 bx lr @ return 0 for NaN +END nterp_f2l_doconv // Entrypoints into runtime. NTERP_TRAMPOLINE nterp_get_static_field, NterpGetStaticField @@ -1975,7 +1978,7 @@ NTERP_TRAMPOLINE nterp_load_object, NterpLoadObject // within [ExecuteNterpImpl, EndExecuteNterpImpl). %def instruction_end(): - .type artNterpAsmInstructionEnd, #object + .type artNterpAsmInstructionEnd, #function .hidden artNterpAsmInstructionEnd .global artNterpAsmInstructionEnd artNterpAsmInstructionEnd: @@ -1986,7 +1989,7 @@ artNterpAsmInstructionEnd: %def instruction_start(): - .type artNterpAsmInstructionStart, #object + .type artNterpAsmInstructionStart, #function .hidden artNterpAsmInstructionStart .global artNterpAsmInstructionStart artNterpAsmInstructionStart = .L_op_nop @@ -1996,6 +1999,8 @@ artNterpAsmInstructionStart = .L_op_nop % return "nterp_" %def opcode_start(): NAME_START nterp_${opcode} + # Explicitly restore CFA, just in case the previous opcode clobbered it (by .cfi_def_*). + CFI_DEF_CFA_BREG_PLUS_UCONST CFI_REFS, -4, CALLEE_SAVES_SIZE %def opcode_end(): NAME_END nterp_${opcode} // Advance to the end of this handler. Causes error if we are past that point. diff --git a/tools/check_cfi.py b/tools/check_cfi.py new file mode 100755 index 0000000000..ac258c28aa --- /dev/null +++ b/tools/check_cfi.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 The Android Open Source Project +# +# 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. + +""" +Checks dwarf CFI (unwinding) information by comparing it to disassembly. +It is only a simple heuristic check of stack pointer adjustments. +Fully inferring CFI from disassembly is not possible in general. +""" + +import os, re, subprocess, collections, pathlib, bisect, collections +from typing import List, Optional, Set, Tuple + +Source = collections.namedtuple("Source", ["addr", "file", "line", "flag"]) + +def get_source(lib: pathlib.Path) -> List[Source]: + """ Get source-file and line-number for all hand-written assembly code. """ + + proc = subprocess.run(["llvm-dwarfdump", "--debug-line", lib], + encoding='utf-8', + capture_output=True, + check=True) + + section_re = re.compile("^debug_line\[0x[0-9a-f]+\]$", re.MULTILINE) + filename_re = re.compile('file_names\[ *(\d)+\]:\n\s*name: "(.*)"', re.MULTILINE) + line_re = re.compile('0x([0-9a-f]{16}) +(\d+) +\d+ +(\d+)' # addr, line, column, file + ' +\d+ +\d +(.*)') # isa, discriminator, flag + + results = [] + for section in section_re.split(proc.stdout): + files = {m[1]: m[2] for m in filename_re.finditer(section)} + if not any(f.endswith(".S") for f in files.values()): + continue + lines = line_re.findall(section) + results.extend([Source(int(a, 16), files[fn], l, fg) for a, l, fn, fg in lines]) + return sorted(filter(lambda line: "end_sequence" not in line.flag, results)) + +Fde = collections.namedtuple("Fde", ["addr", "end", "data"]) + +def get_fde(lib: pathlib.Path) -> List[Fde]: + """ Get all unwinding FDE blocks (in dumped text-based format) """ + + proc = subprocess.run(["llvm-dwarfdump", "--debug-frame", lib], + encoding='utf-8', + capture_output=True, + check=True) + + section_re = re.compile("\n(?! |\n)", re.MULTILINE) # New-line not followed by indent. + fda_re = re.compile(".* FDE .* pc=([0-9a-f]+)...([0-9a-f]+)") + + results = [] + for section in section_re.split(proc.stdout): + m = fda_re.match(section) + if m: + fde = Fde(int(m[1], 16), int(m[2], 16), section) + if fde.addr != 0: + results.append(fde) + return sorted(results) + +Asm = collections.namedtuple("Asm", ["addr", "name", "data"]) + +def get_asm(lib: pathlib.Path) -> List[Asm]: + """ Get disassembly for all methods (in dumped text-based format) """ + + proc = subprocess.run(["llvm-objdump", "--disassemble", lib], + encoding='utf-8', + capture_output=True, + check=True) + + section_re = re.compile("\n(?! |\n)", re.MULTILINE) # New-line not followed by indent. + sym_re = re.compile("([0-9a-f]+) <(.+)>:") + + results = [] + for section in section_re.split(proc.stdout): + sym = sym_re.match(section) + if sym: + results.append(Asm(int(sym[1], 16), sym[2], section)) + return sorted(results) + +Cfa = collections.namedtuple("Cfa", ["addr", "cfa"]) + +def get_cfa(fde: Fde) -> List[Cfa]: + """ Extract individual CFA (SP+offset) entries from the FDE block """ + + cfa_re = re.compile("0x([0-9a-f]+): CFA=([^\s:]+)") + return [Cfa(int(addr, 16), cfa) for addr, cfa in cfa_re.findall(fde.data)] + +Inst = collections.namedtuple("Inst", ["addr", "inst", "symbol"]) + +def get_instructions(asm: Asm) -> List[Inst]: + """ Extract individual instructions from disassembled code block """ + + data = re.sub(r"[ \t]+", " ", asm.data) + inst_re = re.compile(r"([0-9a-f]+): +(?:[0-9a-f]{2} +)*(.*)") + return [Inst(int(addr, 16), inst, asm.name) for addr, inst in inst_re.findall(data)] + +CfaOffset = collections.namedtuple("CfaOffset", ["addr", "offset"]) + +def get_dwarf_cfa_offsets(cfas: List[Cfa]) -> List[CfaOffset]: + """ Parse textual CFA entries into integer stack offsets """ + + result = [] + for addr, cfa in cfas: + if cfa == "WSP" or cfa == "SP": + result.append(CfaOffset(addr, 0)) + elif cfa.startswith("WSP+") or cfa.startswith("SP+"): + result.append(CfaOffset(addr, int(cfa.split("+")[1]))) + else: + result.append(CfaOffset(addr, None)) + return result + +def get_infered_cfa_offsets(insts: List[Inst]) -> List[CfaOffset]: + """ Heuristic to convert disassembly into stack offsets """ + + # Regular expressions which find instructions that adjust stack pointer. + rexprs = [] + def add(rexpr, adjust_offset): + rexprs.append((re.compile(rexpr), adjust_offset)) + add(r"sub sp,(?: sp,)? #(\d+)", lambda m: int(m[1])) + add(r"add sp,(?: sp,)? #(\d+)", lambda m: -int(m[1])) + add(r"str \w+, \[sp, #-(\d+)\]!", lambda m: int(m[1])) + add(r"ldr \w+, \[sp\], #(\d+)", lambda m: -int(m[1])) + add(r"stp \w+, \w+, \[sp, #-(\d+)\]!", lambda m: int(m[1])) + add(r"ldp \w+, \w+, \[sp\], #(\d+)", lambda m: -int(m[1])) + add(r"vpush \{([d0-9, ]*)\}", lambda m: 8 * len(m[1].split(","))) + add(r"vpop \{([d0-9, ]*)\}", lambda m: -8 * len(m[1].split(","))) + add(r"v?push(?:\.w)? \{([\w+, ]*)\}", lambda m: 4 * len(m[1].split(","))) + add(r"v?pop(?:\.w)? \{([\w+, ]*)\}", lambda m: -4 * len(m[1].split(","))) + + # Regular expression which identifies branches. + jmp_re = re.compile(r"cb\w* \w+, 0x(\w+)|(?:b|bl|b\w\w) 0x(\w+)") + + offset, future_offset = 0, {} + result = [CfaOffset(insts[0].addr, offset)] + for addr, inst, symbol in insts: + # Previous code branched here, so us that offset instead. + # This likely identifies slow-path which is after return. + if addr in future_offset: + offset = future_offset[addr] + + # Add entry to output (only if the offset changed). + if result[-1].offset != offset: + result.append(CfaOffset(addr, offset)) + + # Adjust offset if the instruction modifies stack pointer. + for rexpr, adjust_offset in rexprs: + m = rexpr.match(inst) + if m: + offset += adjust_offset(m) + break # First matched pattern wins. + + # Record branches. We only support forward edges for now. + m = jmp_re.match(inst) + if m: + future_offset[int(m[m.lastindex], 16)] = offset + return result + +def check_fde(fde: Fde, insts: List[Inst], srcs, verbose: bool = False) -> Tuple[str, Set[int]]: + """ Compare DWARF offsets to assembly-inferred offsets. Report differences. """ + + error, seen_addrs = None, set() + cfas = get_cfa(fde) + i, dwarf_cfa = 0, get_dwarf_cfa_offsets(cfas) + j, infered_cfa = 0, get_infered_cfa_offsets(insts) + for inst in insts: + seen_addrs.add(inst.addr) + while i+1 < len(dwarf_cfa) and dwarf_cfa[i+1].addr <= inst.addr: + i += 1 + while j+1 < len(infered_cfa) and infered_cfa[j+1].addr <= inst.addr: + j += 1 + if verbose: + print("{:08x}: dwarf={:4} infered={:4} {:40} // {}".format( + inst.addr, str(dwarf_cfa[i].offset), str(infered_cfa[j].offset), + inst.inst.strip(), srcs.get(inst.addr, ""))) + if dwarf_cfa[i].offset is not None and dwarf_cfa[i].offset != infered_cfa[j].offset: + if inst.addr in srcs: # Only report if it maps to source code (not padding or literals). + error = error or "{:08x} {}".format(inst.addr, srcs.get(inst.addr, "")) + return error, seen_addrs + +def check_lib(lib: pathlib.Path): + assert lib.exists() + IGNORE = [ + "art_quick_throw_null_pointer_exception_from_signal", # Starts with non-zero offset. + "art_quick_generic_jni_trampoline", # Saves/restores SP in other register. + "nterp_op_", # Uses calculated CFA due to dynamic stack size. + "$d.", # Data (literals) interleaved within code. + ] + fdes = get_fde(lib) + asms = collections.deque(get_asm(lib)) + srcs = {src.addr: src.file + ":" + src.line for src in get_source(lib)} + seen = set() # Used to verify the we have covered all assembly source lines. + + for fde in fdes: + if fde.addr not in srcs: + continue # Ignore if it is not hand-written assembly. + + # Assembly instructions (one FDE can cover several assembly chunks). + all_insts, name = [], None + while asms and asms[0].addr < fde.end: + asm = asms.popleft() + if asm.addr < fde.addr: + continue + insts = get_instructions(asm) + if any(asm.name.startswith(i) for i in IGNORE): + seen.update([inst.addr for inst in insts]) + continue + all_insts.extend(insts) + name = name or asm.name + if not all_insts: + continue # No assembly + + # Compare DWARF data to assembly instructions + error, seen_addrs = check_fde(fde, all_insts, srcs) + if error: + print("ERROR at " + name + " " + error) + check_fde(fde, all_insts, srcs, True) + print("") + seen.update(seen_addrs) + for addr in sorted(set(srcs.keys()) - seen): + print("Missing CFI for {:08x}: {}".format(addr, srcs[addr])) + + +def main(argv): + """ Check libraries provided on the command line, or use the default build output """ + + libs = argv[1:] + if not libs: + out = os.environ["OUT"] + libs.append(out + "/symbols/apex/com.android.art/lib/libart.so") + libs.append(out + "/symbols/apex/com.android.art/lib64/libart.so") + for lib in libs: + check_lib(pathlib.Path(lib)) + +if __name__ == "__main__": + main(os.sys.argv) |