Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Compiler changes printf to puts

Consider the following code:

#include <stdio.h>

void foo() {
    printf("Hello world\n");
}

void bar() {
    printf("Hello world");
}

The assembly produced by both these two functions is:

.LC0:
        .string "Hello world"
foo():
        mov     edi, OFFSET FLAT:.LC0
        jmp     puts
bar():
        mov     edi, OFFSET FLAT:.LC0
        xor     eax, eax
        jmp     printf

Now I know the difference between puts and printf, but I find this quite interesting that gcc is able to introspect the const char* and figure out whether to call printf or puts.

Another interesting thing is that in bar, compiler zero'ed out the return register (eax) even though it is a void function. Why did it do that there and not in foo?

Am I correct in assuming that compiler 'introspected my string', or there is another explanation of this?

like image 243
skgbanga Avatar asked Dec 06 '22 08:12

skgbanga


2 Answers

Am I correct in assuming that compiler 'introspected my string', or there is another explanation of this?

Yes, this is exactly what happens. It's a pretty simple and common optimization done by the compiler.

Since your first printf() call is just:

printf("Hello world\n");

It's equivalent to:

puts("Hello world");

Since puts() does not need to scan and parse the string for format specifiers, it's quite faster than printf(). The compiler notices that your string ends with a newline and does not contain format specifiers, and therefore automatically converts the call.

This also saves a bit of space, since now only one string "Hello world" needs to be stored in the resulting binary.

Note that this is not possible in general for calls of the form:

printf(some_var);

If some_var is not a simple constant string, the compiler cannot know if it ends in \n.

Other common optimizations are:

  • strlen("constant string") might get evaluated at compile time and converted into a number.
  • memmove(location1, location2, sz) might get transformed into memcpy() if the compiler is sure that location1 and location2 don't overlap.
  • memcpy() of small sizes can be converted in a single mov instruction, and even if the size is larger the call can sometimes be inlined to be faster.

Another interesting thing is that in bar, compiler zero'ed out the return register (eax) even though it is a void function. Why did it do that there and not in foo?

See here: Why is %eax zeroed before a call to printf?


Related interesting posts

  • Why doesn't GCC optimize this call to printf?
  • Can printf get replaced by puts automatically in a C program?
  • Why it shows puts when I disassemble no matter whether I'm using printf or puts?
  • Difference between printf@plt and puts@plt
  • -O2 optimizes printf("%s\n", str) to puts(str)
like image 200
Marco Bonelli Avatar answered Dec 28 '22 07:12

Marco Bonelli


Another interesting thing is that in bar, compiler zero'ed out the return register (eax) even though it is a void function. Why did it do that there and not in foo?

This is completely unrelated to the question in the title, but is interesting none the less.

The xor zeroing %eax is before the call to printf so is part of the call and has nothing to do with the return value. The reason this happens is that printf is a varargs function, and the x86_64 ABI for varargs function requires passing floating-point arguments in xmm registers, and requires passing the number of such arguments in %al. So this instruction is there to ensure that %al is 0 as no arguments are being passed in xmm registers to printf.

puts is not a varargs function, so it is not required there.

like image 22
Chris Dodd Avatar answered Dec 28 '22 05:12

Chris Dodd