There is no dark magic in computing—computers have no flesh and blood.
Anyone who believes in computer dark magic is a muggle.
—Me

cdqe

The Symptom

While debugging with a colleague, we encountered what seemed like “computer sorcery”:

    syscallnr = rec->nr;
    printf("syscallnr=%d\n", syscallnr);
    printf("In renderxxx\nsyscall table address: %p\n", syscalls);

    // causes seg fault
    entry = get_syscall_entry(syscallnr, false);

    // executes normally
    // entry = syscalls[syscallnr].entry;

    printf("entry: %p\n", entry);
    printf("syscall name: %s\n", entry->name);
    printf("arg1name: %s\n", entry->arg1name);

In the code above, get_syscall_entry causes a segmentation fault. But commenting out that line and using the direct array access instead works perfectly. Yet the function implementation is identical to the commented-out line:

struct syscallentry * get_syscall_entry(unsigned int callno, bool do32)
{
    printf("In getxxx\nsyscall table address: %p\n", syscalls);
    printf("In getxxx\nentry: %p\n", syscalls[callno].entry);

    return syscalls[callno].entry;
}

Tracing the Root Cause

The output showed: img

Comparing the two entry addresses: the arrow marks the value after the return. The return itself changed the value. This seemed like sorcery—return syscalls[callno].entry shouldn’t alter the value. The direct assignment works fine. Let’s debug.

img

At the point of return, get_syscall_entry hasn’t modified the return value (stored in rax): img

But after the return—something goes wrong! Notice that rax has been mangled. The culprit? A mysterious cdqe instruction! img

Checking the documentation: cdqe performs a signed extension of EAX into RAX. This means the upper bits of RAX get sign-extended (potentially zeroed or set), exactly matching the observed behavior.

Digging Deeper

Now that we know cdqe is the culprit, the question becomes: why is the compiler emitting an instruction I never wrote?

After extensive research, I found an Intel blog post describing various pitfalls in 64-bit Linux development. Example 7 closely matches our issue.

In C, you can use functions without preliminary declaration.

So C lets you use functions without declaring them first. But since the compiler doesn’t know the function’s return type, it defaults to int and emits a signed extension. The fix is simple: properly declare the function prototype in a header file.

How do you permanently prevent this kind of issue? Stop using C.