assemblyx86global-variableslocal-variablesi386

In x86 assembly, when should I use global variables instead of local variables?


I am creating some small programs with x86 assembly, and it's my first time using a low level language so I'm not used to it.

In high level languages I rarely use global variables, but I've seen a lot of tutorials using global variables in assembly, so I'm not sure when to use global variables instead of local variables.

By global variables I mean data created in the .bss and .data segments, and by local variables I mean data allocated on the stack of the current procedure, using the stack pointer.

Right now, I am using local variables, and parameters a lot more than global variables.

Thanks in advance.


Solution

  • Yes, prefer locals that you keep in registers, or on the stack if needed.

    "variable" is a high level concept that doesn't truly exist in asm. So it's just a question of where you keep the data that you're working on. But sure, local vs. global var is a good way to talk about static storage (.data / .bss / .rodata) vs. stack memory, if you're thinking in terms of un-optimized C where every variable really does have an address in memory.

    In asm, code with fewer instructions is usually easier to understand. Removing store/reload mov instructions in favour of just keeping data in registers usually makes it easier to follow. The joy of writing in asm is finding ways to do the same job with fewer (and/or cheaper) instructions, and uselessly storing/reloading to memory is the opposite of that. It makes your code ugly, IMO.


    Globals suck in asm for all the reasons they suck in higher level languages (unclear data flow when functions read/write them), and other considerations that you might not think about in a high-level language: every instruction that uses a static address like [my_var] has a 4-byte disp32 as part of the addressing mode, vs. [esp+8] only needing 2 extra bytes (SIB because of ESP as the base, and disp8 because +8 fits in a sign-extended 8-bit integer). Or if you make a stack frame with EBP, you save the SIB byte in addressing modes.

    Globals are maybe justifiable in toy programs that basically are just one function, if you don't care about efficiency and would rather define your memory layout with labels and dd / dw / db instead of just offsets into the stack frame. But often you can just keep everything in registers in that case. (Especially on x86-64, where you have 15 GP registers other than the stack pointer, vs. IA-32 only having 7, or 6 if you dedicate EBP to being a frame pointer.)

    Using lots of globals in asm examples/tutorials is maybe a leftover style habit from old ISAs like 6502 or 8051 that didn't have stack-pointer-relative addressing modes, and thus local variables on the call-stack were a bad thing. (See Why do C to Z80 compilers produce poor code?)

    It's maybe also / instead done as a simple way to name variables for the purpose of making an example, but in asm that's what comments are for. There is no compiler to turn your self-documenting code into efficient code. Or you can do what MSVC's asm output does, and define assemble-time constants for the offset of each local relative to the stack frame. e.g.

    foo equ -12
    func:
       push  ebp
       mov   ebp, esp
       sub   esp, 24
       ...
    
       mov  eax, [ebp + foo]
       leave
       ret
    

    Even better: keep your local variables in registers

    For most variables, there's usually no need to spill them to memory anywhere. Use comments to keep track of which variable or expression is where.

    If it doesn't hurt efficiency, often you can have a 1:1 correspondence between registers and the high-level variables you're thinking about when designing the algorithm. e.g. maybe x stays in edi for the whole function, including in all blocks after branching. (And some other registers are mostly used as scratch space for computations and stuff loaded from memory.)

    In that case you'd have a block of comments at the top of a function documenting this. If some of the registers are set near the top of the function, those source lines can be good places for such comments.


    Memory-destination sub dword [loop_counter], 1 has 6 cycle latency on typical modern x86 ISAs (5 cycle store-forwarding + 1 cycle ALU). If you use this as part of a loop, it will run at best one iteration per 6 cycles. This is part of why C compilers with optimization disabled make such slow code. Doing this by hand is basically shooting yourself in the foot.

    dec ecx / jnz only has 1 cycle latency, so a loop without any store/reload as part of a loop-carried dependency can run as fast as 1 iteration per clock cycle. (For loops of up to 4 uops on current Intel CPUs; up to 5 instructions if the dec/jnz or cmp/jcc at the bottom macro-fuses into a single uop. Otherwise you hit front-end bottlenecks. Speaking of which, memory-destination read-modify-write operations are always at least 2 uops.)


    When to use globals

    Allocating a big array in the BSS is easy for testing stuff. Then you can get the address into a register with mov edi, array in NASM syntax, or mov edi, OFFSET array in MASM syntax. So you can use that to test code that's written to take a pointer to an array as an input.

    (On some Linux kernel versions, anonymous hugepages might work differently for BSS vs. mmapped regions, but I think they do work on BSS regions in modern kernels. Not on file-backed .data / .rodata regions, though, except maybe if you dirty the private .data pages, essentially them anonymous.)

    Static constant data is useful

    The most common use-case is probably strings in section .rodata (or section .rdata on Windows).

    section .rodata                     ; linked as part of the TEXT segment
    msg: db "Hello World", 10
    msglen equ $ - msg                  ; assemble-time constant
    

    Often you need a string in memory to pass by reference to a system call like write or a function like puts or printf (e.g. as a format string). Having it in read-only memory and materializing a pointer is a lot easier than storing a string to memory from immediates, like with push `rld\n` or

    mov dword [esp], "Hell"
    mov dword [esp+4], "o Wo"
    ...
    mov ecx, esp              ; pointer to the string