Reputation: 419
Does 0x00000000004005c7 <+28>: movw $0x0,0x8(%rsp)
add the null character at the end of the string?
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:
vendor_id : GenuineIntel
cpu family : 6
model : 63
model name : Intel(R) Xeon(R) CPU @ 2.30GHz
Upvotes: 1
Views: 4218
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