To learn assembly I am viewing the assembly generated by GCC using the -S command for some simple c programs. I have an add function which accepts some ints and some char and adds them together. I am just wondering why the char parameters are pushed onto the stack as 8 bytes (pushq)? Why not just push a single byte?
.file "test.c"
.text
.globl add
.type add, @function
add:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl %edx, -12(%rbp)
movl %ecx, -16(%rbp)
movl %r8d, -20(%rbp)
movl %r9d, -24(%rbp)
movl 16(%rbp), %ecx
movl 24(%rbp), %edx
movl 32(%rbp), %eax
movb %cl, -28(%rbp)
movb %dl, -32(%rbp)
movb %al, -36(%rbp)
movl -4(%rbp), %edx
movl -8(%rbp), %eax
addl %eax, %edx
movl -12(%rbp), %eax
addl %eax, %edx
movl -16(%rbp), %eax
addl %eax, %edx
movl -20(%rbp), %eax
addl %eax, %edx
movl -24(%rbp), %eax
addl %eax, %edx
movsbl -28(%rbp), %eax
addl %eax, %edx
movsbl -32(%rbp), %eax
addl %eax, %edx
movsbl -36(%rbp), %eax
addl %edx, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size add, .-add
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
pushq $9
pushq $8
pushq $7
movl $6, %r9d
movl $5, %r8d
movl $4, %ecx
movl $3, %edx
movl $2, %esi
movl $1, %edi
call add
addq $24, %rsp
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size main, .-main
.ident "GCC: (Ubuntu 4.9.2-10ubuntu13) 4.9.2"
.section .note.GNU-stack,"",@progbits
#include <stdio.h>
int add(int a, int b, int c, int d, int e, int f, char g, char h, char i)
{
return a + b + c + d + e + f + g + h + i;
}
int main()
{
return add(1, 2, 3, 4, 5, 6, 7, 8, 9);
}
It's like that because the x86-64 SystemV ABI requires it.
See https://github.com/hjl-tools/x86-psABI/wiki/x86-64-psABI-r252.pdf for a copy of the current version of the spec. See also the x86 tag wiki for links to ABIs (and much more good stuff.)
See page 17 of the abi PDF:
Classification The size of each argument gets rounded up to eightbytes. (footnote: Therefore the stack will always be eightbyte aligned).
Further (pg 16: Stack Frame):
The end of the input argument area shall be aligned on a 16 (32, if
__m256
is passed on stack) byte boundary. In other words, the value (%rsp + 8
) is always a multiple of 16 (32) when control is transferred to the function entry point.
If they'd designed it so different integer types had different widths on the stack, but 8-byte types were still always 8-byte aligned, there would be complicated rules about where the padding goes, (and thus where the called function finds its args) depending on the types of current and previous args. And it would mean variadic functions like printf would need a different calling convention that didn't pack the args.
8-bit pushes are not encodable at all. Only 16-bit (with a 0x66
prefix), or 64-bit (no prefix, or REX.W=1
) are available. Intel manual is a bit confusing on this, implying in the text that push r32
is encodeable in 64-bit mode (maybe with REX.W=0), but that is not the case: See
How many bytes does the push instruction pushes onto the stack when I don't specify the operand size?.
When pushing values onto the stack, the push must always be based on the word size of the system. If you're an old timer like me, that's 16 bits (though I do have some 12 bit word size systems!), but it really is system dependent.
Since you're talking about X86_64, you will be talking about 64 bit words. My understanding is that the word size is typically connected to the minimum number of bytes required to address any value on the RAM of the system. Since you have a 64 bit memory space, a 64 bit (or 8 bytes, a "quad word" based on the original 16 bit word size) is required.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With