dud3
dud3

Reputation: 419

Null character assembly code

  1. Does 0x00000000004005c7 <+28>: movw $0x0,0x8(%rsp) add the null character at the end of the string?

  2. Could someone also explain the first 4 lines?

    0x00000000004005ab <+0>:     sub    $0x28,%rsp
    0x00000000004005af <+4>:     mov    %fs:0x28,%rax
    0x00000000004005b8 <+13>:    mov    %rax,0x18(%rsp)
    0x00000000004005bd <+18>:    xor    %eax,%eax
    0x00000000004005bf <+20>:    movq   $0x64636261,(%rsp)
    0x00000000004005c7 <+28>:    movw   $0x0,0x8(%rsp)
    => 0x00000000004005ce <+35>:    mov    %rsp,%rdi
    0x00000000004005d1 <+38>:    callq  0x40059d <func>
    0x00000000004005d6 <+43>:    mov    $0x0,%eax
    0x00000000004005db <+48>:    mov    0x18(%rsp),%rdx
    0x00000000004005e0 <+53>:    xor    %fs:0x28,%rdx
    0x00000000004005e9 <+62>:    je     0x4005f0 <main+69>
    0x00000000004005eb <+64>:    callq  0x400480         <__stack_chk_fail@plt>
    0x00000000004005f0 <+69>:    add    $0x28,%rsp
    0x00000000004005f4 <+73>:    retq   
    

C code:

#include <stdio.h>

void func(char s[])
{
    printf("%s\n", s);
}

int main()
{
    char s[10] = "abcd";

    func(s);

    return 0;
}

Thanks.


OS:

CPU:

Upvotes: 1

Views: 4218

Answers (1)

Cody Gray
Cody Gray

Reputation: 244802

Yes, this adds the NUL character to the end of the string. Actually, it is zero-padding the entire character array---read on for more details.

It should be obvious from reading that individual instruction that it is storing a 0 somewhere in memory, although you couldn't tell that it was actually putting it at the end of the string.

movw   $0x0,0x8(%rsp)

You can see here that this instruction does a word move. Specifically, it moves the immediate value 0 ($0x0) to the memory location 0x8(%rsp), which is an 8-byte offset from the address in the rsp register.

If you expand the context in which you examine the code, things become even more clear. Consider the previous instruction:

movq   $0x64636261,(%rsp)

This does a quad-word move of the immediate value 0x64636261 to the memory location stored in the rsp register. That immediate value, of course, is the string "abcd".

Now, a character is a single byte, and 0x64636261 is 4 bytes, just like the string "abcd". Why in the world is an 8-byte move being done here? Well, because the compiler is taking advantage of implicit zero-extension behavior. When it uses a quad-word move instruction with a double-word immediate, the double-word immediate is implicitly zero extended to a quad word. So what you are actually doing is moving 0x0000000064636261 to (%rsp).

The word move instruction is also zero-extended: the one-byte immediate value is implicitly zero-extended to a full word, and then the word 0x0000 is moved into memory at 0x8(%rsp).

All together, then we've moved 10 bytes into memory: 8 bytes from the quad-word move, and 2 bytes from the word move. This number 10 should look familiar---it is the size of the s array that you declared in your C code!

There is a basic rule of the C language that says:

"If there are … fewer characters in a string literal that is used to initialize an array of known size than there are elements in the array, the remainder of the [array] shall be initialized implicitly the same as objects that have static storage duration."

(C99 $6.7.8/21)

That effectively means that the rest of the array is filled with 0s.

The first 4 bytes of that array are filled with your string, "abcd", and then the next 6 bytes are filled with 0s. The assembly code just breaks the stores up the most optimal way possible: first, it does the largest possible store, and then it does the largest possible store that it can get away with without overrunning the maximum length of the array.


As for the rest of the code, let's walk through it line-by-line:

  • sub $0x28,%rsp

rsp is the register that contains the stack pointer. This is subtracting 0x28 bytes from the stack pointer, effectively reserving 40 bytes of space on the stack for the function to use locally. It uses 10 bytes or so explicitly; the rest of the space is probably required by the calling convention or is allocated as an optimization to maintain alignment.

  • mov %fs:0x28,%rax

This retrieves the value from %fs:0x28 and stores it in %rax. fs is a segment register, and 0x28 is an offset. Modern 32-bit and 64-bit operating systems don't use segmented addressing like the old 16-bit real mode required, but fs is commonly used for thread-local storage. So the code is reading the value at offset 0x28 from the beginning of the thread-local storage block, and placing it in the rax register.

  • mov %rax,0x18(%rsp)

This stores the value from rax (the one we just loaded in there) into memory. Specifically, it loads it onto the stack at an offset of 0x18 from the stack pointer (rsp).

I'm guessing that these two lines of code implement some type of stack canary, but I can't be certain without more information about your operating system, compiler settings, etc. My compiler doesn't generate code like this when I compile your code.

  • xor %eax,%eax

This one's simple, but a bit obscure. Bitwise XOR-ing a register with itself is an old trick for zeroing that register's contents. It's also by far the most optimal way of doing it, so that's the code that all compilers will generate.

Now, it might look a little bit strange that it's only zeroing the 32-bit eax register, instead of the whole 64-bit rax register, but it in fact, it is doing that. Virtually all instructions that operate on 32-bit registers in long mode implicitly zero the upper 32-bit half of the register. This is an important optimization at an architectural level, and since the compiler knows the processor is going to do it, it emits code that takes advantage of it. The 32-bit XOR instruction is smaller and thus faster than if it had emitted xor %rax,%rax, but the behavior is identical.

Why is the compiler emitting code to clear the rax/eax register? Because, in all x86 calling conventions that I'm aware of, that register is used for the function's return value. Your main function returns 0, so the compiler is arranging for that return value to be in the rax register.

Upvotes: 4

Related Questions