-
Notifications
You must be signed in to change notification settings - Fork 246
Description
I encountered an issue when compiling libccv with CompCert 3.16 on a Raspberry Pi 5 (Arm Cortex A76) running Raspberry Pi OS.
Some of the functions in libccv are apparently large enough to cause ccomp to generate tbnz instructions whose target label is outside the allowed range (+= 32KB offset from the branch instruction, according to Arm documentation). For example, the code generated for function ccv_sobel in file ccv_basic.c is rejected by the assembler with the following message:
ccv_basic.s: Assembler messages:
ccv_basic.s:203: Error: conditional branch out of range
ccv_basic.s:29086: Error: conditional branch out of range
ccomp: error: assembler command failed with exit code 1 (use -v to see invocation)
Line 203 contains the instruction
tbnz w14, #31, .L104and label .L104 is way down on line 28494. The full assembly output is here.
Instructions for reproducing the error with libccv on an rpi5:
- Download libccv stable release and unzip
- Enter
lib/directory - In
config.mk.in, setCC := ccompand add-fstruct-passing -flongdoubleto CFLAGS - Run
./configure && make
I've seen the same problem with tbz in a local fork of CompCert that inserts extra code (so the functions are even larger), but so far with unmodified CompCert I've only seen it with tbnz.
My bandaid fix is to extend the expand_instruction function in aarch64/Asmexpand.ml to rewrite conditional branches to use an unconditional long jump in the branching case, as described here and here.
The code looks like this (covering all conditional branch instructions, maybe unnecessarily):
let is_cond_branch = function
| Pbc _ | Ptbnz _ | Ptbz _ | Pcbnz _ | Pcbz _ -> true
| _ -> false
let negate_cond = function
| TCeq -> TCne
| TCne -> TCeq
| TChs -> TClo
| TClo -> TChs
| TCmi -> TCpl
| TCpl -> TCmi
| TChi -> TCls
| TCls -> TChi
| TCge -> TClt
| TClt -> TCge
| TCgt -> TCle
| TCle -> TCgt
(** Negate a conditional branch instruction and update its target
label to [new_tgt]. *)
let negate_cond_branch new_tgt = function
| Pbc (cond, _) -> Pbc (negate_cond cond, new_tgt)
| Ptbnz (sz, r, i, _) -> Ptbz (sz, r, i, new_tgt)
| Ptbz (sz, r, i, _) -> Ptbnz (sz, r, i, new_tgt)
| Pcbnz (sz, r, _) -> Pcbz (sz, r, new_tgt)
| Pcbz (sz, r, _) -> Pcbnz (sz, r, new_tgt)
| _ -> raise (Error "negate_cond_branch: expected conditional branch")
let tgt_of_branch = function
| Pbc (_, tgt) -> tgt
| Ptbnz (_, _, _, tgt) -> tgt
| Ptbz (_, _, _, tgt) -> tgt
| Pcbnz (_, _, tgt) -> tgt
| Pcbz (_, _, tgt) -> tgt
| _ -> raise (Error "tgt_of_branch: expected conditional branch")
(** Emit long-jump version of [instr]. *)
let expand_cond_branch instr : unit =
let lbl = new_label () in
emit (negate_cond_branch lbl instr);
emit (Pb (tgt_of_branch instr));
emit (Plabel lbl)
let expand_instruction instr =
match instr with
| (* ... Existing cases ... *)
| _ when is_cond_branch instr ->
expand_cond_branch instr
| _ ->
emit instrThis solution is obviously not ideal since it rewrites all conditional branches when it is very rare that they require it.
It's also not clear to me if this should handled prior to Asmexpand, within the verified part of the compiler. It looks like the semantics in aarch64/Asm.v doesn't take range limitations of conditional branches into account.