3

Let's say I create a program in C like this and compile it on a regular 64-bit desktop machine:

#include <stdint.h>

int main(void)
{
    uint64_t a = 0x12345679abcdefULL;
    uint64_t b = 0xfdcba987654321ULL;
    uint64_t c = a + b;
    return 0;
}

I just tested the code using gcc -O0 -S. It seems to allocate two 32-bit values on the stack and add then with addl and adcl separately. This happens for both -m32 and -m64 switch, even though I am on a 64-bit machine, and there indeed seem to be a dedicated instruction set for 64-bit integers, including operations such as movq and addq.

  1. Why did gcc produce code like for a 32-bit machine even if I told it to use 64-bit arithmetic with -m64?

    >uname -a
    CYGWIN_NT-6.2-WOW work 1.7.35(0.287/5/3) 2015-03-04 12:07 i686 Cygwin
    
    >gcc --version
    gcc (GCC) 4.9.2
    

Now, let's say I'm on a 32-bit machine and I operate on 64-bit integers.

  1. The program allocated two 32-bit variables on the stack. Is this part of a C/C++ standard, or is allocating them on the heap to be expected with some compilers (because they try to fit a 64-bit integer into a 32-bit stack in a different way)?

  2. If I put movq in a program designed for a 32-bit machine, will the desired behavior be emulated, or will the instruction be misunderstood?

1 Answer 1

6

1:

See the Immediates section on this 64 bit assembly reference.

Immediate values inside instructions remain 32 bits and their value is sign extended to 64 bits

There just isn't an instruction to move a 64 bit immediate value to memory (*). And because it can be emulated nicely by moving two chunks of 32 bit, there was no reason to introduce new 64 bit instructions. (They could have changed the immediate movs to always use 64 bits. But considering that most constants you'll ever use are <= 2^31, using 32 bit only saves a lot of space for the upper zero bytes, and costs a bit when you actually use larger constants, so this saves memory).

(*) However, there are instructions to move a 64 bit immediate value to the 64 bit registers, because you can't access the high 32 bit in registers, opposed to memory.

I don't know why your program produced separate addl/adcl instructions; this is what i got from your program:

    pushq   %rbp
    movq    %rsp, %rbp
    movl    $2041302511, -24(%rbp)
    movl    $1193046, -20(%rbp)
    movl    $-2023406815, -16(%rbp)
    movl    $16632745, -12(%rbp)
    movq    -16(%rbp), %rax
    movq    -24(%rbp), %rdx
    leaq    (%rdx,%rax), %rax
    movq    %rax, -8(%rbp)
    movl    $0, %eax
    leave
    ret

As you can see, the leaq (%rdx,%rax), %rax adds 64 bit numbers all right. This was a gcc 4.4.7 on a RHEL 6.6 64 bit system. Please, always state your compiler and OS version, as the output may be quite dependent on those.

2:

As long as you're dealing with an x86/amd64 architecture, you can probably rely on local variables being put on the stack, and global variables not on the stack. But please note the concept of 'stack' and 'heap' aren't as clearly defined as it would seem. The brk/sbrk mechanism of allocating memory is deprecated; modern implementations use mmap. This might mean you have several small heaps in different sections of your address space. On ARM and MIPS, there's no stack pointer at all - there's just a convention that one specific register serves as the stack pointer, but the instructions to push/pop would work with other registers as well(*). In theory, the compiler is free to do a mmap() at the start of each function to allocate local memory, and munmap() it at the end of the function. The only thing the compiler must do is not keep the memory allocated (for reasonable definitions of allocated) after the function exits.

(*) This is a bit of an oversimplification but demonstrates the concept.

Of course, the idea of using mmap() to make space for local variables is an extreme example, that probably noone uses. But lots of compilers put local variables into processor registers and never reserve space on the stack for them (if you never use a pointer to them, and on architectures that aren't as register-starved as x86). Many architectures use processor registers for function arguments as well. And i've seen microcontroller C compilers that allow you to put all variables local to a function into a static area, if you use a certain keyword so the compiler knows the function isn't called recursively. So, while most of the time, local variables will be placed on the stack, you shouldn't assume this is carved in stone.

3.

The instruction will be misunderstood. The processor can be in 32 bit or 64 bit mode, and the same instructions (in the meaning of: the same sequence of instruction bytes) have different meanings in each of them. For example, 48 89 43 ec is mov [rbx-20],rax in 64 bit mode, but dec eax; mov DWORD PTR [ebx-0x14],eax in 32 bit mode.

2
  • Very well presented answer, thanks! Two further questions arise: regarding 1 - is there anything I can read about how specifically gcc decides which instruction set (64/32 bit) to use? Regarding 2 - using mmap sounds to be much more expensive than moving a stack pointer (standardized or not). Do ARM or MIPS compilers stray from the convention you've mentioned?
    – rr-
    Mar 13, 2015 at 15:04
  • Which instruction set gcc uses depends on -m32 or -m64. Which exact instructions within that instruction set are used depends on (at least) compiler version number and optimization; i don't think there's a comprehensive documentation on this except the source code itself. I amended my answer a bit in respect to the local variables conventions. Mar 13, 2015 at 16:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.