Marc Marc - 1 year ago 54
C Question

Volatile/modified return address

Consider a C function (with external linkage) like the following one:

void f(void **p)
/* do something with *p */

Now assume that
is being called in a way such that
points to the return address of
on the stack, as in the following code (assuming the System V AMD64 ABI):

leaq -8(%rsp), %rdi
callq f

What may happen is that the code of
modifies the return address on the stack by assigning a value to *p. Thus the compiler will have to treat the return address on the stack as a volatile value. How can I tell the compiler, gcc in my case, that the return address is volatile?

Otherwise, the compiler could, at least in principle, generate the following code for

pushq %rbp
movq 8(%rsp), %r10
pushq %r10
## do something with (%rdi)
popq %r10
popq %rbp
addq 8,%rsp
jmpq *%r10

Admittedly, it is unlikely that a compiler would ever generate code like this but it does not seem to be forbidden without any further function attributes. And this code wouldn't notice if the return address on the stack is being modified in the middle of the function because the original return address is already retrieved at the beginning of the function.

P.S.: As has been suggested by Peter Cordes, I should better explain the purpose of my question: It is about garbage collecting dynamically generated machine code using a moving garbage collector: The function
stands for the garbage collector. The callee of
may be a function whose code is being moved around while
is running, so I came up with the idea of letting
know the return address so that
may modify it accordingly to whether the memory area the return address points to has been moved around or not.

Answer Source

Using the SysV ABI (Linux, FreeBSD, Solaris, Mac OS X / macOS) on AMD64/x86-64, you only need a trivial assembly function wrapped around the actual garbage collector function.

The following f.s defines void f(void *), and calls the real GC, real_f(void *, void **), with the added second parameter pointing to the return address.

    .file       "f.s"

    .p2align    4,,15
    .globl      f
    .type       f, @function

    movq        %rsp, %rsi
    call        real_f

    .size       f, .-f

If real_f() already has two other parameters, use %rdx (for the third) instead of %rsi. If three to five, use %rcx, %r8, or %r9, respectively. SysV ABI on AMD64/x86-64 only supports up to six non-floating-point parameters in registers.

Let's test the above with a small example.c:

#include <stdlib.h>
#include <stdio.h>

extern void f(void *);

void real_f(void *arg, void **retval)
    printf("real_f(): Returning to %p instead of %p.\n", arg, *retval);
    *retval = arg;

int main(void)
    printf("Function and label addresses:\n");
    printf("%p f()\n", f);
    printf("%p real_f()\n", real_f);
    printf("%p one_call:\n", &&one_call);
    printf("%p one_fail:\n", &&one_fail);
    printf("%p one_skip:\n", &&one_skip);



    printf("At one_fail.\n");

    printf("At one_skip.\n");

    return EXIT_SUCCESS;

Note that the above relies on both GCC behaviour (&& providing the address of a label) as well as GCC behaviour on AMD64/x86-64 architecture (object and function pointers being interchangeable), as well as the C compiler not making any of the myriad optimizations they are allowed to do to the code in main().

(It does not matter if real_f() is optimized; it's just that I was too lazy to work out a better example in main(). For example, one that creates a small function in an executable data segment that calls f(), with real_f() moving that data segment, and correspondingly adjusting the return address. That would match OP's scenario, and is just about the only practical use case for this kind of manipulation I can think of. Instead, I just hacked a crude example that might or might not work for others.)

Also, we might wish to declare f() as having two parameters (they would be passed in %rdi and %rsi) too, with the second being irrelevant, to make sure the compiler does not expect %rsi to stay unchanged. (If I recall correctly, the SysV ABI lets us clobber it, but I might remember wrong.)

On this particular machine, compiling the above with

gcc -Wall -O0 f.s example.c -o example

running it



Function and label addresses:
0x400650 f()
0x400659 real_f()
0x400729 one_call:
0x400733 one_fail:
0x40074c one_skip:

real_f(): Returning to 0x40074c instead of 0x400733.
At one_skip.

Note that if you tell GCC to optimize the code (say, -O2), it will make assumptions about the code in main() it is perfectly allowed to do by the C standard, but which may lead to all three labels having the exact same address. This happens on my particular machine and GCC-5.4.0, and of course causes an endless loop. It does not reflect on the implementation of f() or real_f() at all, only that my example in main() is quite poor. I'm lazy.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download