From now on we will no longer have to use C code as kind of a pseudocode description for our assembler programs. Instead we can use the C code as an equivalent, more convenient formulation of our programs. Following some rules the C code can be rewritten (or compiled) into assembly code. Later this will be done by a tool, a C compiler, at the moment you have to do it. By doing so you learn the C programming language by example and you will get an idea how a C compiler works.
Function main and the subprogram _start
By convention every program has a function main that returns an integer. When a program gets executed it behaves as if function main is the first function that gets executed. The return value of main defines the exit code of the program. If main has no explicit return statement this function returns 0 by default. Note that such a default return value is only guarantied for function main.
For example, the following program only defines a function main which explicitly returns 42:
In C you are not allowed to use the identifier _start for a function, this identifier is reserved. The gory details are that in general a program has to communicate with the operating system for receiving arguments and returning an exit code. This communication happens through so called system calls, and different operating systems have different system calls even if they run on the same hardware. So _start is reserved for being the name of a function that actually gets called first and itself calls function main. Platform depended system calls can be done in _start before and after main gets called. In the above example the implementation of function _start was added by the linker. With the command nm you can display the entries of a program's symbol table. You can use nm to see that the compiled programs have the symbol _start:
theon$ nm main | grep start
0000000000600b10 B __bss_start
U __fpstart@@SYSVABI_1.3
0000000000400660 T __start_crt
0000000000400600 T _start
theon$
The ULM does not have an operating system but we use _start to guarantee that the stack is initialized before function main gets called.
Equivalent assembly programs
The following assembly program is equivalent to the above C program in main.c (which was returning 42 in function main). It also contains the implementation of the _start function.
Until the linker gets covered in the next session we write all into the same file but in a way that we later can split this single source file into separate compile units. Hence for each function the directives for arguments etc. are repeated:
The function body of procedure puts can be described by a flow chart and it can accordingly be rewritten with spaghetti code (i.e. it contains goto statements) for reflecting more closely how you can implement it in assembly:
// ... directives for arguments, locals, etc..text
main:
// ... function prologue ...movq %RET, ret(%SP)
movq %FP, fp(%SP)
addq0, %SP, %FP
// reserve space for local variables.subq0*8, %SP, %SP
/* statements *//* return 0; */ldzwq0, %4movq %4, rval(%FP)
// ... function epilogue ...addq0, %FP, %SP
movq fp(%SP), %FP
movq ret(%SP), %RET
jmp %RET, %0
Whenever you see a string literal in a C program it means the compiler will generate in the data segment a corresponding .string directive with a unique label. So for before we consider how to call puts we generate the string with some unique label:
1
"hello, word!\n"
1
2
3
.data
.main.L0: // some unique label.string"hello, word!\n"
When you pass a string to a function you only pass a pointer to the string as argument. Function puts does not return a value, hence we applying the recipe for calling a procedure with one argument (which is the pointer to the string literal) we have
Next comes the implementation of puts which is described by:
1
2
3
4
5
6
7
voidputs(char*str){while(*str){putchar(*str++);}}
This procedure expects one parameter str which should be a pointer to a character, i.e. str should be the address of the string's first character. In C terminology that is expressed by declaring str as a variable of type char *, and you see this declaration (bookkeeping information) in the lines
1
2
voidputs(char*str)
When you use the variable in a statement *str denotes the value at the end of the pointer, in this case a character.
We can now dig into the details of implementing function puts. It does not return a value so we use the recipe for implementing a procedures:
// ... directives for arguments, locals, etc..equ str, proc_arg0
.text
puts:
// function prologuemovq %RET, ret(%SP)
movq %FP, fp(%SP)
addq0, %SP, %FP
// reserve space for local variables.subq0*8, %SP, %SP
// begin of the function body/* Implementation of the function or procedure */// end of the function body// function epilogueaddq0, %FP, %SP
movq fp(%SP), %FP
movq ret(%SP), %RET
jmp %RET, %0
The implementation of the body was described by the flow chart above. And we implement this chart node by node.
Conditional jump if (*str == 0)
One thing I dislike in C, and many other programming languages, is that the comparison for equality is expressed with “==” and the assignment with “=”. I would prefer a single equal sign for comparison and something like “:=” for assignments. But knowing the meaning of “==” in
is that *str (the character at the end of pointer str) is compared with zero. So you first have to load the pointer str, i.e. the address stored in the argument str into a register
1
movq str(%FP), %4
and then the character at the end of the pointer:
1
movzbq (%4), %4
after that you can check if %4 contains zero and jump in that case. The code for this node is
the label puts_while is needed so that we can later jump back to it, and puts_done is a label to jump to the end of the function body.
Printing the character *str
It would be a shame to call here a function putchar to print a single character. So we just load *str and use the putc instruction:
1
putchar(*str);
1
2
3
movq str(%FP), %4movzbq (%4), %4putc %4
Of course it is also a shame that we reload *str into %4. Because before *str was already fetched into %4. But at the moment we just blindly implement the flow chart without much thinking. We can care about optimizations another time.
Incrementing the pointer str
Now we increment str so that it points to the next character
1
str =str+1;
1
2
3
movq str(%FP), %4addq1, %4, %4movq %4, str(%FP)
Unconditional jump and label to break the loop
The unconditional jump is a single instruction
1
goto puts_while;
1
jmp puts_while
and the empty statement is just a label after the unconditional jump
This C program implements it's own strlen function for determining the length of a string. For the sake of simplicity the program just returns the string length in main:
typedefunsignedlonguint64_t;// unsigned long is 64 bit wide on theonuint64_tstrlen(char*str){char*ch=str;while(*ch){++ch;}returnch-str;}intmain(){returnstrlen("hello, world!\n");}
Step by step explanation what this C code describes
We again begin with function main:
1
2
3
4
5
intmain(){returnstrlen("hello, world!\n");}
As the code contains a string literal we choose some unique label and generate the string in the data segment. For function main we use the skeleton for a function:
// ... directives for arguments, locals, etc..text
main:
// ... function prologue ...movq %RET, ret(%SP)
movq %FP, fp(%SP)
addq0, %SP, %FP
// reserve space for local variables.subq0*8, %SP, %SP
/* statements */// ... function epilogue ...addq0, %FP, %SP
movq fp(%SP), %FP
movq ret(%SP), %RET
jmp %RET, %0
Function main only contains a return statement. The return value is an expression which in turn is defined as the return value of a function call. So we call function strlen, store the return value on the stack and jump to the epilogue of main:
Note that in this case the jump to the epilogue is an unnecessary instruction as the epilogue follows immediately. You see that when we put things together:
// ... directives for arguments, locals, etc..text
main:
// ... function prologue ...movq %RET, ret(%SP)
movq %FP, fp(%SP)
addq0, %SP, %FP
// reserve space for local variables.subq0*8, %SP, %SP
/* return strlen("hello, word!\n"); */subq32, %SP, %SP
ldzwq .main.L0, %4movq %4, func_arg0(%SP)
ldzwq strlen, %4jmp %4, %RET
movq rval(%SP), %4movq %4, rval(%FP)
addq32, %SP, %SP
// define a label before the epiloguejmp .main.leave
.main.leave:
// ... function epilogue ...addq0, %FP, %SP
movq fp(%SP), %FP
movq ret(%SP), %RET
jmp %RET, %0
In general a return statement can occur in the middle of a compound statement. Using this “jump to the epilogue pattern” allows us to implement return statement in a mindless way that always works. For example in cases like this:
1
2
3
4
5
if(condition){returna;}else{returnb;}
Again, optimizing the assembly code is something we can do afterwards. First of all we need something that just does the job, and a method to derive such a working solution.
But let's not digress, the next thing where we need a working solution is function strlen. In a first step we just care about the coarse structure, i.e. what arguments and local variables does the functions. The initialization of local variable is uninteresting in this case so we rewrite:
// ... directives for arguments, locals, etc..equ str, func_arg0
.equ ch, local0
.text
puts:
// function prologuemovq %RET, ret(%SP)
movq %FP, fp(%SP)
addq0, %SP, %FP
// reserve space for local variables.subq1*8, %SP, %SP // for ch// begin of the function body/* Implementation of the function or procedure */// end of the function body// function epilogueaddq0, %FP, %SP
movq fp(%SP), %FP
movq ret(%SP), %RET
jmp %RET, %0
In your bookkeeping you have to note that variables str and ch are supposed to be pointer to a character. That means each of these variables stores a 64 bit address of a character. That means *str and *ch in the C code refer to a character at the end of the pointer.
Initialization of the local variable
We simply fetch the value of variable str and store it at the memory location for variable ch. The bookkeeping note tell us that both variables have the size of 8 bytes, so we use movq for fetching and storing:
1
ch=str;
1
2
3
4
5
/* ch = str */movq str(%FP), %4movq %4, ch(%FP)
While loop
We rewrite the while loop with spaghetti code and also resolve the meaning of the post-increment:
Looking at the bookkeeping notes we recall that *ch refers to the byte with the address stored in the local variable ch. This byte gets compared against zero in the conditional jump: