3

I have a C code like this one, that will be possibly compiled in an ELF file for ARM:

int a;
int b=1;

int foo(int x) {
    int c=2;
    static float d=1.5;
    // .......
}

I know that all the executable code goes into the .text section, while .data , .bss and .rodata will contain the various variables/constants. My question is: does a line like int b=1; here add also something to the .text section, or does it only tell the compiler to place a new variable initialized to 1 in .data (then probably mapped in RAM memory when deployed on the final hardware)?

Moreover, trying to decompile a similar code, I noticed that a line such as int c=2;, inside the function foo(), was adding something to the stack, but also some lines of .text where the value '2' was actually memorized there.

So, in general, does a declaration always imply also something added to .text at an assembly level? If yes, does it depends on the context (i.e. if the variable is inside a function, if it is a local global variable, ...) and what is actually added?

Thanks a lot in advance.

es483
  • 361
  • 2
  • 16
  • 1
    "`.data`, `.bss` and `.rodata` will contain the various variables/constants" - only static and/or global variables. Non-static local variables will not go into either one of these segments, nor to any other segment. They are merely "slots in the stack". A "non-static local variable declaration" is not added anywhere. A "non-static local variable declaration + initialization" is added to the code segment (just the initialization of course). – goodvibration Jan 30 '18 at 08:43
  • 1
    If `int b = 1;` is at file scope, it doesn't add to the text section. If it is inside a function, then some code has to initialize `b` to `1` each time execution passes its definition. – Jonathan Leffler Jan 30 '18 at 09:15

3 Answers3

1

As @goodvibration correctly stated, only global or static variables go to the segments. This is because their lifetime is the whole execution time of the program.

Local variables have a different lifetime. They exist only during the execution of the block (e.g. function) they are defined within. If a function is called, all parameters that does not fit into registers a pushed to the stack and the return address is written to the link register.* The function saves possibly the link register and other registers at the stack and adds some space at the stack for local variables (this is the code you have observed). At the end of the function, the saved registers are poped and the the stackpointer is readjusted. In this way, you get an automatic garbage collection for local variables.

*: Please note, that this is true for (some calling conventions of) ARM only. It's different e.g. for Intel processors.

Matthias
  • 8,018
  • 2
  • 27
  • 53
1

does a line like int b=1; here add also something to the .text section, or does it only tell the compiler to place a new variable initialized to 1 in .data (then probably mapped in RAM memory when deployed on the final hardware)?

You understand that this is likely to be implementation specific, but the likelihood is that that you will just get initialised data in the data section. Were it a constant, it might, instead go into the text section.

Moreover, trying to decompile a similar code, I noticed that a line such as int c=2;, inside the function foo(), was adding something to the stack, but also some lines of .text where the value '2' was actually memorized there.

Automatic variables that are initialised, have to be initialised each time the function's scope is entered. The space for c is reserved on the stack (or in a register, depending on the ABI) but the program has to remember the constant from which it is initialised and this is best placed somewhere in the text segment, either as a constant value or as a "move immediate" instruction.

So, in general, does a declaration always imply also something added to .text at an assembly level?

No. If a static variable is initialised to zero or null or not initialised at all, it is often just enough to reserve space in bss. If a static non constant variable is initialised to a non zero value, it will just be put in the data segment.

JeremyP
  • 84,577
  • 15
  • 123
  • 161
  • 1
    There is at least some compilers that will but data in `rodata` for the automatic variables. Ie, you have a very complex structure, using move immediate maybe more expensive than copying data to the stack. However, the answer is correct for typical cases. This 'data' may also be placed with the function in the 'text' section as part of a literal pool. Complex constants (lots of bit changes in the '32 bit value') on traditional ARM are like this. – artless noise Jan 30 '18 at 19:39
1

this is one of those just try it things.

int a;
int b=1;
int foo(int x) {
    int c=2;
    static float d=1.5;
    int e;
    e=x+2;
    return(e);
}

first thing without optimization.

arm-none-eabi-gcc -c so.c -o so.o
arm-none-eabi-objdump -D so.o
arm-none-eabi-ld -Ttext=0x1000 -Tdata=0x2000 so.o -o so.elf
arm-none-eabi-ld: warning: cannot find entry symbol _start; defaulting to 0000000000001000
arm-none-eabi-objdump -D so.elf > so.list

do worry about the warning, needed to link to see that everything found a home

Disassembly of section .text:

00001000 <foo>:
    1000:   e52db004    push    {r11}       ; (str r11, [sp, #-4]!)
    1004:   e28db000    add r11, sp, #0
    1008:   e24dd014    sub sp, sp, #20
    100c:   e50b0010    str r0, [r11, #-16]
    1010:   e3a03002    mov r3, #2
    1014:   e50b3008    str r3, [r11, #-8]
    1018:   e51b3010    ldr r3, [r11, #-16]
    101c:   e2833002    add r3, r3, #2
    1020:   e50b300c    str r3, [r11, #-12]
    1024:   e51b300c    ldr r3, [r11, #-12]
    1028:   e1a00003    mov r0, r3
    102c:   e28bd000    add sp, r11, #0
    1030:   e49db004    pop {r11}       ; (ldr r11, [sp], #4)
    1034:   e12fff1e    bx  lr

Disassembly of section .data:

00002000 <b>:
    2000:   00000001    andeq   r0, r0, r1

00002004 <d.4102>:
    2004:   3fc00000    svccc   0x00c00000

Disassembly of section .bss:

00002008 <a>:
    2008:   00000000    andeq   r0, r0, r0

as a disassembly it tries to disassemble data so ignore that (the andeq next to 0x2008 for example).

The a variable is global and uninitialized so it lands in .bss (typically...a compiler can choose to do whatever it wants so long as it implements the language correctly, doesnt have to have something called .bss for example, but gnu and many others do).

b is global and initialized so it lands in .data, had it been declared as const it might land in .rodata depending on the compiler and what it offers.

c is a local non-static variable that is initialized, because C offers recursion this needs to be on the stack (or managed with registers or other volatile resources), and initialized each run. We needed to compile without optimization to see this

1010:   e3a03002    mov r3, #2
1014:   e50b3008    str r3, [r11, #-8]

d is what I call a local global, it is a static local so it lives outside the function, not on the stack, alongside the globals but with local access only.

I added e to your example, this is a local not initialized, but then used. Had I not used it and not optimized there probably would have been space allocated for it but no initialization.

save x on the stack (per this calling convention x enters in r0)

100c:   e50b0010    str r0, [r11, #-16]

then load x from the stack, add two, save as e on the stack. read e from the stack and place in the return location for this calling convention which is r0.

1018:   e51b3010    ldr r3, [r11, #-16]
101c:   e2833002    add r3, r3, #2
1020:   e50b300c    str r3, [r11, #-12]
1024:   e51b300c    ldr r3, [r11, #-12]
1028:   e1a00003    mov r0, r3

For all architectures, unoptimized this is somewhat typical, always read variables from the stack and put them back quickly. Other architectures have different calling conventions with respect to where the incoming parameters and outgoing return value live.

If I optmize (-O2 on the gcc line)

Disassembly of section .text:

00001000 <foo>:
    1000:   e2800002    add r0, r0, #2
    1004:   e12fff1e    bx  lr

Disassembly of section .data:

00002000 <b>:
    2000:   00000001    andeq   r0, r0, r1

Disassembly of section .bss:

00002004 <a>:
    2004:   00000000    andeq   r0, r0, r0

b is a global, so at the object level a global space has to be reserved for it, it is .data, optimization doesnt change that.

a is also global and still .bss, because at the object level it was declared such so allocated in case another object needs it. The linker doesnt remove these.

Now c and d are dead code they dont do anything they need no storage so c is no longer allocated space on the stack nor is d allocated any .data space.

We have plenty of registers for this architecture for this calling convention for this code, so e does not need any memory allocated on the stack, it comes in in r0 the math can be done with r0 and then it is returned in r0.

I know I didnt tell the linker where to put .bss by telling it .data it put .bss in the same space without complaint. I could have put -Tbss=0x3000 for example to give it its own space or just done a linker script. Linker scripts can play havoc with the typical results, so beware.

Typical, but there might be a compiler with exceptions:

non-constant globals go in .data or .bss depending on whether they are initialized during the declaration or not. If const then perhaps .rodata or .text depending (or .data or .bss would technically work)

non-static locals go in general purpose registers or on the stack as needed (if not completely optimized away).

static locals (if not optimized away) live with globals but are not globally accessible they just get allocated space in .data or .bss like the globals do.

parameters are governed completely by the calling convention used by that compiler for that target. Just because arm or mips or other may have written down a convention doesnt mean a compiler has to use it, only if they claim to support some convention or standard should they then attempt to comply. For a compiler to be useful it needs a convention and stick to it whatever it is, so that both caller and callee of a function know where to get parameters and to return a value. Architectures with enough registers will often have a convention where some few number of registers are used for the first so many parameters (not necessarily one to one) and then the stack is used for all other parameters. likewise a register may be used if possible for a return value. Some architectures due to lack of gprs or other, use the stack in both directions. or the stack in one and a register in the other. You are welcome to seek out the conventions and try to read them, but at the end of the day the compiler you are using, if not broken follows a convention and by setting up experiments like the one above you can see the convention in action.

Plus in this case optimizations.

void more_fun ( unsigned long long );
unsigned fun ( unsigned int x, unsigned long long y )
{
    more_fun(y);
    return(x+1);
}

If I told you that arm conventions typically use r0-r3 for the first few parameters you might assume that x is in r0 and r1 and r2 are used for y and we could have another small parameter before needing the stack, well perhaps older arm, but now it wants the 64 bit variable to use an even then an odd.

00000000 <fun>:
   0:   e92d4010    push    {r4, lr}
   4:   e1a04000    mov r4, r0
   8:   e1a01003    mov r1, r3
   c:   e1a00002    mov r0, r2
  10:   ebfffffe    bl  0 <more_fun>
  14:   e2840001    add r0, r4, #1
  18:   e8bd4010    pop {r4, lr}
  1c:   e12fff1e    bx  lr

so r0 contains x, r2/r3 contain y and r1 was passed over.

the test was crafted to not have y as dead code and to pass it to another function we can see where y was stored on the way into fun and way out to more_fun. r2/r3 on the way in, needs to be in r0/r1 to call more fun.

we need to preserve x for the return from fun. one might expect that x would land on the stack, which unoptimized it would, but instead save a register that the convention has stated will be preserved by functions (r4) and use r4 throughout the function or at least in this function to store x. A performance optimization, if x needed to be touched more than once memory cycles going to the stack cost more than register accesses.

then it computes the return and cleans up the stack, registers.

IMO it is important to see this, the calling convention comes into play for some variables and others can vary based on optimization, no optimization they are what most folks are going to state off hand, .bss, .data (.text/.rodata), with optimization then it depends if if the variable survives at all.

old_timer
  • 69,149
  • 8
  • 89
  • 168
  • Thank you for your very interesting answer, that inspired me in making some tests in the next days. It is very interesting to study what the compiler does when doing/not doing optimization! Before, I directly compiled with Kinetis Design Studio and its settings for the compilation, but I think I'll move directly to calling arm-none-eabi-gcc in order to "experiment" a bit also with the various compiler flags. – es483 Jan 30 '18 at 20:03
  • try as many compilers as you have access to or are interested in gaining access. no reason to assume any two compilers or versions produce the same code. the high level is it .data or .bss stuff should be somewhat consistent but actual code production can vary across compilers or versions. expect it rather than be surprised by it. – old_timer Jan 30 '18 at 22:05