5

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);
}
Ross Ridge
  • 38,414
  • 7
  • 81
  • 112
chasep255
  • 11,745
  • 8
  • 58
  • 115
  • 6
    The stack must be aligned to the word size of the system. So it wouldn't make sense to be able to push a single byte unless that was the word size. – Jeff Mercado Jul 19 '15 at 05:30
  • I do not seen any `char`s. – alk Jul 19 '15 at 07:27
  • @alk - in the generated asm, neither does the op, hence the question. The `char`s are parameters of the `add` function in the code snippet. ;) – enhzflep Jul 19 '15 at 07:55
  • @enhzflep: "*The `char`s are parameters of the add function in the code snippet.*" No, they aren't. `1, 2, 3, 4, 5, 6, 7, 8, 9` are interger literals, which go as `int` by definition. Yes, this does not explain why the compiler uses twice their size on the stack. – alk Jul 19 '15 at 07:58
  • @alk - okay, so how would you describe parameters `g`, `h` and `i` then? Also, by my reckoning, `rbp` is 8 times the width of `al`. – enhzflep Jul 19 '15 at 08:04
  • Ups, I indeed missed the last three, my bad. Sry! Still `7, 8, 9` are in integer literals, `int`s. And as I said in my previous comment: *"this does not explain why the compiler uses twice [quatuble] their size on the stack.*" That's why I do not "answer", but "only" comment here. @enhzflep There surely are others with more expertice on what happens on asm level. – alk Jul 19 '15 at 08:22
  • 3
    @chasep255 - the code you show is x64 code. Therefore, the word size is 64 bits or 8 bytes. This is the smallest container of data that should be put onto the stack. Whether or not the container is full is irrelevant. – enhzflep Jul 19 '15 at 09:00
  • 2
    @alk: It doesn't matter if they're `int` literals or not --- they're all implicitly converted to `char`, because that's what the parameters are. Also, note that @enhzflep was talking about *parameters*, and you were talking about *arguments* (and before type conversion, at that). – Tim Čas Jul 19 '15 at 10:47
  • 1
    Better compile with `gcc -O1 -S -fverbose-asm` – Basile Starynkevitch Jul 22 '15 at 04:25

2 Answers2

10

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 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?.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • It's probably not just the ABI. I don't think you can actually push (using a PUSH opcode) anything but full register size (e.g. EAX for 32 bit or RAX for 64 bit) in x86-32 or x86-64 mode (but I'm prepared to be proven wrong). You could of course emulate pushes of smaller sizes by simply placing the values on the stack and updating the stack pointer, but that is not really pushing, IMO. – Rudy Velthuis Jul 20 '15 at 16:18
  • Yeah, you're right. `PUSH r/m32` and `PUSH r32` are not encodable in 64bit mode. Only 64 and 16bit (operand-size prefix) pushes are available. 8b pushes aren't available at all. – Peter Cordes Jul 20 '15 at 16:40
  • Why 16 bit and not 32 bit. Also why not 1 byte. – chasep255 Jul 21 '15 at 17:37
  • 16bit is encoded with an operand-size prefix before the opcode. 8-bit for most instructions is encoded with a different opcode (because for 8086 -> 80286, they made new opcodes instead of changing the default size for existing ones, I think). And there is no 8-bit `push` opcode. – Peter Cordes Jul 22 '15 at 04:08
  • Why no 32? IDK, I think they could have had the `W` bit of the `REX` prefix allow a 32bit operand size for `push`/`pop`. Actually, the text description says this is supported. It just doesn't show up in the table of opcodes. – Peter Cordes Jul 22 '15 at 04:13
3

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.

David Hoelzer
  • 15,862
  • 4
  • 48
  • 67