• Please review our updated Terms and Rules here

x86 Assembly Local Variables

neilobremski

Experienced Member
Joined
Oct 9, 2016
Messages
55
Location
Seattle, USA
The SS register stores the Stack Segment and the logical stack itself grows down. When you PUSH a register, its value is added at SS:SP and SP is decremented by 2. [SUP]1[/SUP] A procedure's local variables, then, are allocated below SP by the amount of bytes required to store them.

Let's start by looking at a simple C function that merely declares some local variables, manipulates them, and then returns ...

Code:
void locals(void)
{
	char x, y, z;
	short a, b, c, d;
	a = b + c * d;
	x += y - z;
}

Firstly the declaration of local variables does not issue individual machine instructions per variable. Those first two lines declare to the C compiler that there are three char variables (8-bit) and four short integers (16-bit). For space, however, the machine code generated is just a subtraction to SP (remember the stack grows downward!) relative to the amount of space needed for the local variables. [SUP]2[/SUP]

A second thing that needs to happen is the stack's base pointer BP is set to the current stack pointer SP (after being pushed to the stack itself of course; for backup). The reason for this is that the machine code will need to reference the local variables by a constant offset and SP is unsuited for this because it's always changing as pushes and pops occur. By making the offset relative to BP it can remain the same across the entire procedure.

Let's look at how Microsoft C 5.10 produces the assembler with /Ox optimizations enabled (I've added most of the Line # comments):

Code:
; Line 1 void locals(void)
_locals	PROC NEAR
; Line 2 {
	push	bp
	mov	bp,sp
; Line 3 char x, y, z;
; Line 4 short a, b, c, d;
	sub	sp,14
;	x = -10
;	y = -12
;	z = -14
;	a = -2
;	b = -4
;	c = -6
;	d = -8
; Line 5 a = b + c * d;
	mov	ax,WORD PTR [bp-6]	;c
	imul	WORD PTR [bp-8]	;d
	add	ax,WORD PTR [bp-4]	;b
	mov	WORD PTR [bp-2],ax	;a
; Line 6 x += y - z;
	mov	al,BYTE PTR [bp-12]	;y
	sub	al,BYTE PTR [bp-14]	;z
	add	BYTE PTR [bp-10],al	;x
; Line 7 }
	mov	sp,bp
	pop	bp
	ret	
	nop

_locals	ENDP

That last part where the function ends may look somewhat mysterious. Why isn't SP incremented by 14 since it was decremented by that much earlier? The answer is that it is replaced to its original value by copying BP which would be the same as adding 14. However, this way of doing it could be "safer" because any stack corruption that occurred within the function or one of its calls would be erased.

Notice again that all of the local variables are simply negative offsets from the stack base pointer BP. These are consistent offsets because BP is not changed during the course of this function. And if another procedure is called, it will store BP on the stack and restore it before returning so that it remains unchanged for our purposes.

Footnotes:
[SUP]1[/SUP] Only 16-bit values are ever pushed onto the stack with either PUSH or PUSHF (push flags).
[SUP]2[/SUP] Technically this means you'd need 11 bytes but compilers will often word-align the locals so that every char occupies its own 16-bit space! For the example, then, there are 14 bytes "allocated".
 
Blogging about x86 assembly language programming? Awesome! :D

Just a couple of clarifications to clear any possible misunderstandings;
The reason for this is that the machine code will need to reference the local variables by a constant offset and SP is unsuited for this because it's always changing as pushes and pops occur. By making the offset relative to BP it can remain the same across the entire procedure.
It's actually impossible to use the stack pointer to reference memory on pre-386 processors, so even if the compiler is able to keep track of the stack pointer (which it almost always can) it must use BP instead. On 386+ processors, ESP (and only ESP, not SP) can be used to reference memory. For example, that's what the gcc optimization option -fomit-frame-pointer does. See this.

Footnotes:
1 Only 16-bit values are ever pushed onto the stack with either PUSH or PUSHF (push flags).
2 Technically this means you'd need 11 bytes but compilers will often word-align the locals so that every char occupies its own 16-bit space! For the example, then, there are 14 bytes "allocated".
Unless you're running in 32-bit mode. Also, with 186+ processors you can push 8-bit immediate values but they will be sign-extended on the stack. This is the most efficient way (size wise) to set a segment register to point to the Interrupt Vector Table or the BIOS Data Area (unless you know you already have a zeroed general purpose register available);
Code:
	push	BYTE 0	; 2 bytes
	pop	ds	; 1 byte
 
Back
Top