Compilers are very good at generating high quality assembler
code with options for generating optimized code.
So why learn anything about assembler language?
-
Assembler language is very close to the way the machine
executes instructions.
-
Looking at assembler language can show why some features of a
high level language are more efficient than others.
-
Being able to read assembler language contributes to
being able to analyze an unknown executable program without
having the source code.
-
Understand enough details of machine code to exploit
certain security vulnerabilities; for example, the buffer
overflow attack.
Recall that the C compiler command, gcc
actually invokes several other programs to transform a source file
sumFirst.c file into a machine executable program file.
preprocessor (cpp): sumFirst.c ==> sumFirst.i
compiler: sumFirst.i ==> sumFirst.s
assembler (as): sumFirst.s ==> sumFirst.o
linker (ld): sumFirst.o plus other object files ==> sumFirst
C PreProcessor (cpp) is first called to handle #include's,
#ifdef, #define, etc preprocessor statements in the source
code.
The gcc command can execute the preprocessor and then stop
without doing the other steps:
gcc -E sumFirst.c
This produces the file sumFirst.i
The actual C compiler is invoked to translate the output of the
preprocessor into an equivalent assembly language
program (sumFirst.s). Each assembler instruction
corresponds to a machine instruction, but the operation code
(op code) is a human readable string such as "addl" or
"jmp".
The gcc command can produce the assembly file sumFirst.s and
the stop without producing the object file by:
gcc -S sumFirst.c
The assembler (as) translates sumFirst.s to an object file
sumFirst.o
where each instruction now consists of a sequence of bytes
with op codes encoded in as binary. E.g "addl" is replaced
by a single byte, for example 0x01, 0x02, ..., or 0x05. Each
different op code for "addl" determines what form the two operands
will have and where each operand is located (in memory, in
a register).
The gcc command can produce the object file sumFirst.o and stop
without linking:
gcc -c sumFirst.c
The linker (ld) is invoked to combine
sumFirst.o with a C runtime object file
(crt.o) and pre compiled object code for C library
functions such as printf.o to produce an executable
file containing the same machine code instructions, but with
address references to printf, etc., fixed to their actual
offsets within the executable.
The GNU C compiler option -S generates a text file with the
assembler code that is generated by the compiler for a C
source code file.
$ gcc -S sample.c
This generates a file sample.s
1 int sumFirst(int n)
2 {
3 int ans = 0;
4 int i;
5 for(i = 1; i <= n; i++) {
6 ans += i;
7 }
8 return ans;
9 }
In default settings for the gcc C compiler, for loops can't
declare loop variables. That is, a compile error is generated for:
for(int i = 1; i <= n; i++) {
This should be valid in the new C standard (C99), but many
compilers only partially implement C99 including gcc.
So the gcc compiler default
is the earlier standard.
The partial C99 features implemented by
gcc are used when the option -std=c99 is present.
_sumFirst:
pushl %ebp ; Set up the stack for
movl %esp, %ebp ; the local variables
subl $8, %esp ; of the sumFirst function
ans = 0;
movl $0, -4(%ebp) ; -4(%ebp) is ans
i = 1;
movl $1, -8(%ebp) ; -8(%ebp) is i
L2: i <= n
movl -8(%ebp), %eax ; copy i value from memory into register %eax
cmpl 8(%ebp), %eax ; 8(%ebp) is n; compare n and i
jg L3 ; jump to end of for loop (location L3) if n > i
ans += i;
movl -8(%ebp), %eax ; copy i to %eax
leal -4(%ebp), %edx ; copy address of ans into %edx
addl %eax, (%edx) ; add i to ans
i++
leal -8(%ebp), %eax ; load address of i in %eax
incl (%eax) ; increment i
jmp L2 ; jump back to label L2 for loop beginning
L3: return ans;
movl -4(%ebp), %eax ; copy return value, ans, into register %eax
leave ; pop local variables from the stack
ret ; and return to the instruction
; after the call to sumFirst
What if you don't have the source file (sumFirst.c) but do
have the executable file sumFirst or the object file sumFirst.o?
The sumFirst.o file of machine code bytes is generated from
sumFirst.s by the assembler that is called by gcc.
In this case you can't use gcc with the -S option since that
would require having sumFirst.c.
The general utility od lets you examine the bytes of
any file whether they contain printable characters or not.
The od name is short for octal dump, meaning
that each byte of a file is displayed in base 8 notation by
default. However, you can change that to hex or other
formats.
Registers Special Use
%eax
%ecx
%edx
%ebx
%esi
%edi
%esp Stack pointer
%ebp Frame pointer
%eip Program counter
The arithmetic machine instructions such as addl can't
have both its operands be in memory. At least one of them has to
be in a register.
The first 6 registers can usually be used as general purpose
registers.
Registers Special Use
%eax
%ecx
%edx
%ebx
%esi
%edi
%esp Stack pointer
%ebp Frame pointer
%eip Program counter
The registers %ebp and %esp are used to hold
the addresses of the bottom and the top, respectively of the
stack frame of the currently executing function in a program.
Snapshot of memory during execution after main has called sumFirst:
|
Linked machine instructions |
|
Global Initialized Data |
|
Global Initialized Data |
|
Area for dynamic allocation (new or malloc) |
%esp -> |
Stack Frame for sumFirst |
%ebp -> |
|
|
Stack Frame for main |
When a function returns, the %ebp and %esp registers are
reset to point to the stack frame of the calling function.
So effectively, the current stack frame is popped off the
stack at the return.
Where is the return value?
Typically an integer return value is placed in a register and
this is usally register %eax.
That is, %eax is usually thought of as a general
purpose register. But it often has this special use for
a function return value.
int sum(int a, int b)
{
int ans;
ans = a + b;
return ans;
}
Assignment typically uses the mov instruction.
Addition is the addl instruction.
The return value for a function is always expected to be
in register %eax.
So the assembler ret instruction doesn't have an operand. The return value is implicitly whatever is in register %eax.
In the following examples register %eax will be denoted by reg1
int sum(int a, int b) sum:
{
int ans; reg1 = b
ans = a + b; add a, reg // reg = reg + a
return ans; mov reg1, ans
} mov ans, reg1
return
sum: sum:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
reg1 = b movl 12(%ebp), %eax
add a, reg1 addl 8(%ebp), %eax
mov reg1, ans movl %eax, -4(%ebp)
mov ans, reg1 movl -4(%ebp), %eax
return leave
ret
One of the operands of mov must be a register or an immediate value, not a memory location.
-
Moving the value in %eax to memory location for ans and then moving it back to a register (actually the same register), would be avoided with optimization level 2.
There is no direct IA32 assembler instruction for 'if' or 'while'.
The assembler instructions to use are of the following form:
compare ... // result of compare is stored somewhere
if ( result ) goto Label // conditional jumps
goto Label // unconditional jumps
Example: if (x >= y) goto L2
cmpl y,x
jge L2
Note the order of the operands of cmpl!!!
<max(int x, int y)>: int max(int x, int y)
{
int maxVal; int maxVal;
if (x >= y) goto L2 if ( x < y ) {
maxVal = y maxVal = y;
goto L4 } else {
L2: maxVal = x maxVal = x;
L4: return maxVal }
return maxVal;
}
max: <max(int x, int y)>:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl 8(%ebp), %eax reg1 = x
cmpl 12(%ebp), %eax [compare y, reg1]
jge .L2 if (reg1 >= y) goto L2
movl 12(%ebp), %eax reg1 = y
movl %eax, -4(%ebp) maxVal = reg1
jmp .L4 goto L4
.L2: L2:
movl 8(%ebp), %eax reg1 = x
movl %eax, -4(%ebp) maxVal = reg1
.L4: L4:
movl -4(%ebp), %eax reg1 = maxVal
leave
ret return
Notes:
At L2 memory location x is moved to register %eax (it's already there).
Then register %eax value is moved to memory location maxVal. The register is used as a
temporary only because the movl instruction can't have both operands be in memory.
At L4, the value in maxVal is moved back to register %eax. This time register %eax is not used as
a temporary location, but because the return value of a function is always expected to be in register %eax.
Compiled with optimization level 2:
gcc -S -O2 if1.c
max:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx reg2 = x
movl 12(%ebp), %eax reg1 = y
cmpl %edx, %eax [cmp reg2, reg1]
jge .L2 if (reg1 >= reg2) goto L2
movl %edx, %eax reg1 = reg2
.L2: L2:
popl %ebp
ret return
int sumFirst(int n)
{
int sum = 0;
while(n > 0) {
sum += n;
n--;
}
return sum;
}
There is no instruction that directly implements 'while' in IA32.
The instructions available are the same form as used for 'if statements':
compare ...
if ( result ) goto Label // conditional jumps
goto Label // unconditional jumps
int sumFirst(int n) sumFirst:
{
int sum = 0; sum = 0
goto L2
while(n > 0) L3:
reg1 = n
sum += n; sum = sum + R[%eax]
n--; n = n - 1
L2:
} if(n > 0) goto L3
return sum reg1 = sum
return
}
sumFirst: sumFirst:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl $0, -4(%ebp) sum = 0
jmp .L2 goto L2
.L3: L3:
movl 8(%ebp), %eax reg1 = n
addl %eax, -4(%ebp) sum = sum + reg1
subl $1, 8(%ebp) n = n - 1
.L2: L2:
cmpl $0, 8(%ebp) [compare 0, n]
jg .L3 if(n > 0) goto L3
movl -4(%ebp), %eax reg1 = sum
leave
ret return
If an operand is in memory, an instruction might want to know just
the location (i.e., in order to store some value there).
movl %eax, -4(%ebp) ; move contents of %eax to memory location -4(%ebp)
M[-4(%ebp)] = R[%eax]
On the other hand, if the first operand of movl is a memory location, it moves
the contents to the destination register.
movl -4(%ebp), %eax
R[%eax] = M[-4(%ebp)]
An add instruction would need to know both the values stored
at the locations of its operands and the location of the
second operand:
addl %eax, -4(%ebp) ; add contents of memory register %eax to location -4(%ebp).
M[-4(%ebp)] = M[-4(%ebp)] + R[%eax]
Note: In both cases these instructions need the
contents of the first operand!
leal is the Load Effective Address instruction.
The first operand should be a memory location and the second
operand should be a register.
However, in contrast to movl and
addl, leal moves that memory address to the destination register.
leal -4(%ebp), %eax
R[%eax] = -4(%ebp) ; calculate the address -4(%ebp) and
; store that address in register %eax
Compare this with movl:
movl -4(%ebp), %eax
R[%eax] = M[-4(%ebp)] ; calculate the address -4(%ebp) and
; store the contents of
; that address in register %eax
Note registers %ebx and %esi are used in the table below just as
examples.
However, any other registers can be used in the same way.
Name |
Form |
Denotes Memory Address |
Indirect |
(%ebx) |
R[%ebx] |
base + displacement |
8(%ebx) |
R[%ebx] + 8 |
Indexed |
(%ebx, %esi) |
R[%ebx] + R[%esi]) |
Indexed |
4(%ebx, %esi) |
R[%ebx] + R[%esi] + 4 |
Scaled indexed |
(%ebx, %esi, 4) |
R[%ebx] + 4 * R[%esi] |
Scaled indexed |
8(%ebx, %esi, 4) |
R[%ebx] + 4 * R[%esi] + 8 |
Scaled indexed |
8(,%esi, 4) |
4 * R[%esi] + 8 |
Assume the following register contents:
Register |
Contents |
%eax |
0x8000 |
%edx |
3 |
Each of the following IA32 assembler expressions indicate
a memory location.
Fill in the address for each one.
Operand |
Memory Address |
(%eax) |
|
4(%eax) |
|
-4(%eax) |
|
9(%eax, %edx) |
|
(%eax,%edx,1) |
|
(%eax,%edx,4) |
|
0x8000(,%edx,8) |
|
Given the following register and memory contents,
Register |
Contents |
%edx |
0x8000 |
%ebx |
2 |
Memory Address |
Contents |
0x8000 |
0x5 |
0x8004 |
0xA |
0x8008 |
0xF |
fill in the value stored in register %eax after execution of each instruction:
Instruction |
Value stored in %eax |
leal (%edx), %eax |
|
movl (%edx), %eax |
|
leal 4(%edx), %eax |
|
movl 4(%edx), %eax |
|
leal (%edx, %ebx, 4), %eax |
|
movl (%edx, %ebx, 4), %eax |
|