-2

This simple c:

#include <stdio.h>
#include <string.h>
int *add(int a, int b){
    int ar[1];
    int result = a+b;
    memcpy(ar, &result, sizeof(int));
    return ar;
}

int main(){
    int a = add(1,2)[0];
    printf("%i\n",a);
}

is compiled into this:

.text
    .globl  add
    .type   add, @function
add:
    pushq   %rbp    #
    movq    %rsp, %rbp  #,
    movl    %edi, -20(%rbp) # a, a
    movl    %esi, -24(%rbp) # b, b
# a.c:5:    int result = a+b;
    movl    -20(%rbp), %edx # a, tmp91
    movl    -24(%rbp), %eax # b, tmp92
    addl    %edx, %eax  # tmp91, _1
# a.c:5:    int result = a+b;
    movl    %eax, -8(%rbp)  # _1, result
# a.c:6:    memcpy(ar, &result, sizeof(int)); ---I SEE NO CALL INSTRUCTION---
    movl    -8(%rbp), %eax  # MEM[(char * {ref-all})&result], _6
    movl    %eax, -4(%rbp)  # _6, MEM[(char * {ref-all})&ar]
# a.c:7:    return ar;
    movl    $0, %eax    #--THE FUNCTION SHOULD RETURN ADDRESS OF ARRAY, NOT 0. OTHERWISE command terminated
#   lea -4(%rbp), %rax  #--ONLY THIS IS CORRECT, NOT `0`
# a.c:8: }
    popq    %rbp    #
    ret 
    .size   add, .-add
    .section    .rodata
.LC0:
    .string "%i\n"
    .text
    .globl  main
    .type   main, @function
main:
    pushq   %rbp    #
    movq    %rsp, %rbp  #,
    subq    $16, %rsp   #,
# a.c:11:   int a = add(1,2)[0];
    movl    $2, %esi    #,
    movl    $1, %edi    #,
    call    add #
# a.c:11:   int a = add(1,2)[0];
    movl    (%rax), %eax    # *_1, tmp90
    movl    %eax, -4(%rbp)  # tmp90, a
# a.c:12:   printf("%i\n",a);
    movl    -4(%rbp), %eax  # a, tmp91
    movl    %eax, %esi  # tmp91,
    leaq    .LC0(%rip), %rdi    #,
    movl    $0, %eax    #,
    call    printf@PLT  #
    movl    $0, %eax    #, _6
# a.c:13: }
    leave   
    ret 
    .size   main, .-main
    .ident  "GCC: (Debian 8.3.0-6) 8.3.0"
    .section    .note.GNU-stack,"",@progbits

Every function from stdlib like printf or puts are called from GOT (i.e. %rip register holds the address of GOT). But not memcpy, it is like "assembly inline instructions" instead of regular call address. So is memcpy even a symbol? If so, why is it not as argument to call? Is memcpy in GOT table? If so, what is a offset from GOT to that symbol?

milanHrabos
  • 2,010
  • 3
  • 11
  • 45
  • 1
    A compiler could understand what the `memcpy` functionality is and (for simple invocations) just write the assembler. – Paul Ogilvie Jul 09 '20 at 12:18
  • 3
    Can you take the address of `memcpy`? Yes. It must be a real function with exported symbol. Is the compiler enforced to use that? No. It could just do it inline if that might be shorter or quicker. That only applies to well known standard functions. – Gerhardh Jul 09 '20 at 12:18
  • @Gerhardh then tell me, where the name of that symbol is? Every other function are called like `call printf@PLT #`. Memcpy has no such signature – milanHrabos Jul 09 '20 at 12:19
  • 1
    If you do not take the address of a function in your code, there is no need to include it in the object. – Gerhardh Jul 09 '20 at 12:20
  • @Gerhardh, what is the list of "well-known functions from stdlib" the compiler do "inline"? instead of call from GOT? – milanHrabos Jul 09 '20 at 12:21
  • See https://stackoverflow.com/questions/11747891/when-builtin-memcpy-is-replaced-with-libcs-memcpy and [Other Built-in Functions Provided by GCC](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) – Jonathon Reinhart Jul 09 '20 at 12:22
  • 1
    You can look into the C standard which functions are defined. The compiler might inline any of them. You need to take this into account when you want to write an own function with same name as in the standard – Gerhardh Jul 09 '20 at 12:22
  • 4
    By the way, what is returned from `add` is an address of non-static local variable and dereferencing that after returning from `add` will invoke *undefined behavior*. – MikeCAT Jul 09 '20 at 12:48
  • [Here is an example](https://wandbox.org/permlink/M4rPNAqHuYsGQHjp) that your program result in Segmentation Fault. – MikeCAT Jul 09 '20 at 12:51
  • @MikeCAT yes I know. But there is just one instruction, which make it terminate: `movl $0, %eax `, being it this: `lea -4(%rbp), %rax`, then no termination will arise, even with non-static local variable. So the UB is not cause because of *local-ness*, but because the *compiler* "wants" it to terminate. You can change just **one** instruction, and it will normally compile. – milanHrabos Jul 09 '20 at 12:57
  • You are probably compiling without optimizations. Otherwise, your function should get optimized away into just a `return 0;` – th33lf Jul 09 '20 at 13:00
  • @th33lf but the function should not `return 0`, that is the point, no matter optimization. Function: `int *add(int a, int b){` should return address, which I am returning by `return ar`, and that address should be stored in `rax`. But the compiler will `null` it, for no reason – milanHrabos Jul 09 '20 at 13:04
  • 3
    @milanHrabos The compiler is allowed to do _anything it wants_ with code that has formally undefined behavior. You're lucky it didn't generate assembly language that makes demons fly out of your nose. – zwol Jul 09 '20 at 13:11
  • @zwol sorry, but I do not like those statements (althought very popular). The compiler should be deterministic even in UB. the compiler by itself is also written in c, so it must handle these cases as well. No "demons fly out of nose" – milanHrabos Jul 09 '20 at 13:14
  • 2
    I don't actually disagree with that; but the standard is what it is, for now anyway. You might be interested in John Regehr's "[Friendly C](https://blog.regehr.org/archives/1180)" project. (Note however that if I were rewriting the standard I would make this particular scenario a hard compile-time error.) – zwol Jul 09 '20 at 13:17
  • @Gerhardh: I don't think your statement "only applies to well known standard functions" is true. As far as I am aware, *any* function can be inlined, at the compiler's discretion or at the programmer's request (although a compiler is free to deny such a request). – Jongware Jul 09 '20 at 13:27
  • @milanHrabos The compiler is being very deterministic by returning zero. There is no address for a local variable because it is probably in a register. Even if it were on the stack, the address would be invalid once the function returns. So returning zero is way safer than giving you a way to corrupt your own stack. – th33lf Jul 09 '20 at 13:34
  • @th33lf what corruption are you talking about? I can simply change in to `lea -4(%rbp), %rax`, and once out of the stack, I can trouble-free access "wrong" address `-4(%rbp)` back in original function. I know it is UB according to standard, but it **does not** cause corruption (just try to run it - uncomment the instruction and you will see, cpu has no problem to access "old" stack) – milanHrabos Jul 09 '20 at 14:31
  • 1
    There is no 'old-stack'. Anything beyond your stack is simply invalid memory which you are not supposed to access. C doesn't stop you from accessing it. That doesn't mean it isn't wrong. – th33lf Jul 09 '20 at 15:17
  • @th33lf it could be "wrong", but since it works and I do not see any corruption or anything that would harm the process/stack, I see no reason of avoiding it. Once it shows some problem or misbehaving, I will revise it, but since it works and no deamons from nose fly out, your statements are meaningless. – milanHrabos Jul 09 '20 at 15:30
  • 1
    @milanHrabos You are relying on blind luck, chance. Today it works, tomorrow it may not. Your compiler generates code under the assumption that you follow certain rules. If you break those rules, you might get subtle bugs which won't be easy to find. Even in this case, you might call another function which modifies the same stack location, your OS might swap your process out to disk and swap it back in, and it need not preserve any data that is beyond the currently used stack etc.. You can drive without following the traffic rules - the car will run just fine but Good Luck with that! – th33lf Jul 10 '20 at 08:56

1 Answers1

5

So first off, you have a bug:

$ cc -O2 -S test.c
test.c: In function ‘add’:
test.c:7:12: warning: function returns address of local variable

Returning the address of a local variable has undefined behavior, if and only if the caller uses that value; this is why your compiler generated code that returned a null pointer, which will crash the program if used but be harmless otherwise. In fact, my copy of GCC generates only this for add:

add:
        xorl    %eax, %eax
        ret

because that treatment of the return value makes the other operations in add be dead code.

(The "only if used" restriction is also why my compiler generates a warning, not a hard error.)

Now, if I modify your program to have well-defined behavior, e.g.

#include <stdio.h>
#include <string.h>

void add(int *sum, int a, int b)
{
    int result = a+b;
    memcpy(sum, &result, sizeof(int));
}

int main(void)
{
    int a;
    add(&a, 1, 2);
    printf("%i\n",a);
    return 0;
}

then I do indeed see assembly code in which the memcpy call has been replaced by inline code:

add:
    addl    %edx, %esi
    movl    %esi, (%rdi)
    ret

This is a feature of many modern C compilers: they know what some of the C library's functions do, and can inline them when that makes sense. (You can see that in this case the generated code is both smaller and faster than it would have been with an actual call to memcpy.)

GCC lets me turn this feature off with a command-line option:

$ gcc -O2 -ffreestanding test.c
$ sed -ne '/^add:/,/cfi_endproc/{; /^\.LF[BE]/d; /\.cfi_/d; p; }' test.s
add:
    subq    $24, %rsp
    addl    %edx, %esi
    movl    $4, %edx
    movl    %esi, 12(%rsp)
    leaq    12(%rsp), %rsi
    call    memcpy@PLT
    addq    $24, %rsp
    ret

In this mode, the call to memcpy in add is treated the same as the call to printf in main. Your compiler may have similar options.

zwol
  • 135,547
  • 38
  • 252
  • 361
  • Can you please explain the usage and practical usage of option `ffreestanding`? I have normal `0x86` architecture, why should I use this option? But thanks anyway – milanHrabos Jul 09 '20 at 13:11
  • 4
    The intended use for `-ffreestanding` is when compiling programs that will run in a _freestanding environment_ (this is a concept from the C standard, see [N1570 section 4 paragraph 6](http://port70.net/~nsz/c/c11/n1570.html#4p6)) in which the complete C library is not available. Typical examples are bootloaders, operating system kernels, and control software for small devices where a lot of the C library either doesn't make sense or doesn't fit. Of course `memcpy` is very likely to be available even in those environments, but the standard allows it not to be. – zwol Jul 09 '20 at 13:15
  • @milanHrabos: In this case, because it implies `-fno-builtin-memcpy`, and I think `-fno-builtin` in general. You normally don't want to stop the compiler from inlining memcpy, it's much more efficient for small fixed-size copies to inline a couple `mov` instructions. But for this question, asking whether it's a normal function that can be called, that's how you can get that effect for simple source. – Peter Cordes Jul 09 '20 at 19:20