https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94113
Bug ID: 94113
Summary: Apparently incorrect register allocation in inline asm
when using CMOV.
Product: gcc
Version: 9.2.1
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: inline-asm
Assignee: unassigned at gcc dot gnu.org
Reporter: contact at pgazz dot com
Target Milestone: ---
Created attachment 48001
--> https://gcc.gnu.org/bugzilla/attachment.cgi?id=48001&action=edit
The preprocessed C file
Version: gcc (Debian 9.2.1-21) 9.2.1 20191130
System type: Debian GNU/Linux bullseye/sid
Options and complete command-line: gcc -Wall -Wextra -o broken broken.i
Compiler output: no errors or warnings
Preprocessed file: broken.i
The included example (broken.c) illustrates what appears to be a bug in inline
assembly when using the CMOV instruction. Two different C variables are
allocated the same register, causing incorrect behavior.
__asm__ volatile("movl %3, %0;\n\t" \
"cmpl $0, %1;\n\t" \
"cmovne %2, %0;\n\t" \
: "=r"(final_result) \
: "r"(cond), "r"(if_true), "r"(if_false) \
: "cc" \
);
The inline assembly uses CMOV to assign final_result to either if_true or
if_false depending on the cond variable, which is read in from the first
command-line argument and converted via atoi.
I was expecting the behavior to be equivalent to testing the value of cond,
then assigning final_result to either if_true or if_false depending on whether
cond is non-zero or zero respectively. But instead, it always assigns it to
if_true. Looking at the compiler output in broken.s, the eax register seems to
be allocated to both cond and to final_result. In the first line of the inline
assembly movl %ecx, %eax; it looks like final_result is eax. But in the next
line cmpl $0, %eax; it looks like eax is allocated to cond, which has already
been overwritten by the value of if_false in the previous line.
movl$0, -16(%rbp) # final_result
# ...
movl%eax, -12(%rbp) # cond, after atoi
movl$333, -8(%rbp) # if_true
movl$444, -4(%rbp) # if_false
movl-12(%rbp), %eax # cond -> eax
movl-8(%rbp), %edx # if_true -> edx
movl-4(%rbp), %ecx # if_false -> ecx
#APP
# 17 "broken.c" 1
movl %ecx, %eax;# move if_false to eax, which
cmpl $0, %eax; # should be comparing to the value of cond, not
if_false
cmovne %edx, %eax; # conditional move if_true to final_result, but
condition (not equal) is always true
# 0 "" 2
#NO_APP
movl%eax, -16(%rbp) # eax -> final_result
Is the register allocator somehow assuming that the write-only modifier =r
means the value is only written at the end of the inline assembly?
I could circumvent it by explicitly using another register (esi) and adding an
extra MOV from that register as shown below.
__asm__ volatile("movl %3, %%esi;\n\t" \
"cmpl $0, %1;\n\t" \
"cmovne %2, %%esi;\n\t" \
"movl %%esi, %0;\n\t" \
: "=r"(final_result) \
: "r"(cond), "r"(if_true), "r"(if_false) \
: "cc", "esi" \
);
A somewhat similar-looking bug was reported almost 13 years ago, but that
particular bug does not seem to happen anymore:
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=31386
When compiling with optimizations, the bug still exists when running with
./broken 1, which just turns the cond variable to true.