# C Program Tear-Down¶

Read time: 79 minutes (19781 words)

"The programmer must understand the computer, because the computer will not
understand the programmer" -- E. W. Dijkstra


You should know this man’s name:

Back when you first started learning about programming, you should have been taught that there are three fundamental structured forms you had to learn. They were the Sequence, the Decision, and the Loop. Blame Edsger for that. In 1968, Edsger wrote this now famous letter to the editor of the Association of Computing Machinery Journal:

In this letter, Dijkstra proposed eliminating the goto statement, which was an essential part of programming up until then. He had another idea. Dijkstra coined the phrase Structured Programming as a replacement for this “harmful” statement, and his paper caused quite a storm in the world of software development. He also is considered one of the guiding forces who moved Software Engineering into the mainstream of professions.

With apologies to his memory, I am going to violate the cardinal rule of modern programming, and restore the goto statement - but only for one specific reason.

Warning

Never admit that you have used this statement in any gathering of programming professionals. They may lock you in a closet and throw away the key!

## Simple C Program¶

Let’s put together a simple program that incorporates all three basic structures. I am going to use C (not C++) since we do not need the baggage of modern object-oriented programming.

What should the program do? I know! Let’s add up a bunch of numbers. Just to add a twist, I am only going to add up every other number in a list of numbers.

Here is the code I came up with:

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include unsigned int data[] = { 5, 3, 7, 10, 42, 6, 22, 15, 32 }; int cnt; int sum; unsigned int odd = 0; int main( void ) { cnt = 0; while( cnt < 9 ) { if( odd ) sum += data[ cnt ]; odd = !odd; cnt++; printf( "%d %d\n", cnt, sum ); } } 

Now, this is not the finest example of a C program, but it does have all three basic structures. I chose to use a While Loop since that is probably the first loop you were introduced to. It also uses Global Variables, something we are taught to avoid. We are using those to for a simple reason. All the code can access all those variables, something closer to what goes on in the machine. We will learn about that later!

Other than that, this is pretty simple stuff. You should have no problem getting this running:

$gcc -o sum1 sum1.c$ ./sum1
1 0
2 3
3 3
4 13
5 13
6 19
7 19
8 34
9 34


Note

Every version of this program that we create in this tear-down should produce exactly this same output. I will not repeat what you see above, but you should run the code and prove that for yourself!

## Our Goal¶

What I want to do in this lecture is exactly what a compiler does to your program code. I want to convert it into a form the modern Pentium processor can understand. To accomplish this, I am going to use a real standard C compiler, and I will not alter the simple fact that we will be looking at is a fully standard compliant C program. The program is going to morph into something that does not look the same, but it is really is still a C program.

My goal is to convert exactly this program to one that looks so much like Pentium assembly language that the final conversion to a real Pentium assembly language file is trivial!

Note

See what happens when you wake up a 3am with an idea in your head? The first draft of this idea was done by 4am! I really need more sleep!

## Objective-C¶

Back in the late 1980s, a computer scientist named Brad Cox came up with an idea on how to add object orientation to the C programming language. He used the C preprocessor to add new features to the language, that would be transformed into conventional C code by that preprocessor. This was a pretty cool idea, and I visited Brad at his company home in Sandy Hook Connecticut shortly after he founded the company to release his product, now known as Objective-C. That language, like one I was involved in designing: Ada, had the misfortune of coming out at the same time as C++. You know the result, which one are we using today? Still, Brad’s ideas stuck with me, and led to this lecture!

## The Processor Does Not Understand Structures¶

To do our “morphing” work, we need to view the program from the processor’s point of view. Can the processor make sense of this pile of code?

The first problem the processor has with this code is that there is a lot of stuff going on that it has no idea how to handle. Take the while line for example. There is an expression inside some parentheses that needs to be evaluated before we can even decide if we want to loop or not. Then there is the issue of exactly where is the loop body. You see curly braces, the processor has no clue what they mean. You know they surround that loop body! We need to tell the processor where the loop starts and ends.

Based on our training, we know what will happen. We will evaluate that logical expression inside the parentheses, see if it produces a true or false and then either branch around the loop body, or drop into the loop. When we get to the end of the loop body, we will branch back up and evaluate that logical expression all over.

So, in designing the chip, the Pentium architects had to concoct instructions that would cause the processor to do what you think it should do when processing this code. (It really is an illusion - there is no while in the chip!)

### Branching¶

Branching is absolutely vital to being able to create our structures. Well, maybe not the Sequence structure. That one is too simple!

We need two kinds of branching statements, one conditional and one absolute. We will use a conditional branch at the top of the loop, and an absolute branch at the bottom. The conditional one will let us skip over the loop body, if needed. The unconditional one will take us from the bottom of the loop body back to the conditional test at the top.

### Sorry, Edsger!¶

I am going to be forced to use a GOTO statement now. Yes, even though you are NEVER supposed to use it, it is still there. There are rules on exactly where you can use this statement, but basically they make sense. We will add a Label to some line in our code, and direct the processor to GOTO that label. If we need a conditional branch, we will use an if statement to control the goto statement.

Note

I am not going to teach you exactly how to use this statement. They would drum me out of teaching if I did that. Instead, I will show you what works, and leave it at that!

Let’s modify the program so it does this:

  1 2 3 4 5 6 7 8 9 10 11  int main( void ) { cnt = 0; label1: if( cnt >= 9 ) goto label2; if( odd ) sum += data[ cnt ]; odd = !odd; cnt++; printf( "%d %d\n", cnt, sum ); goto label1; label2: printf( "%d %d\n", cnt, sum ); 

Do you see how this is set up. We evaluate the logical expression and use that to decide if we branch around the loop body. We have two goto statements here, the absolute goto at the bottom of the loop body takes us back to the place where we evaluate that expression again.

We managed to get rid of those ugly curly braces, and the while statement and it still works as before.

Unfortunately, we got rid of one structure, but we used another structure in its place. Now, we have two if statements. Hey, we are making progress here!

## Simplifying the Logical Expression¶

Once again, we see a statement with some inner work to do. The first if statement has a complex (!) expression to evaluate before it can decide what to do. Let’s pull that expression out of the statement and evaluate it first. (This will cause us to introduce a new variable to store the result. I know, let’s call it flag:

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int cnt; int sum; int flag; unsigned int odd = 0; int main( void ) { cnt = 0; label1: flag = ( cnt == 9 ); if( flag ) goto label2; if( odd ) sum += data[ cnt ]; odd = !odd; cnt++; printf( "%d %d\n", cnt, sum ); goto label1; label2: printf( "%d %d\n", cnt, sum ); } 

Now, our conditional jump is much simpler. Let’s do the same thing with the second if statement:

  1 2 3 4 5 6 7 8 9 10 11 12 int main( void ) { cnt = 0; label1: flag = ( cnt == 9 ); if( flag ) goto label2; if( !odd ) goto label3; sum += data[ cnt ]; label3: odd = !odd; cnt++; printf( "%d %d\n", cnt, sum ); goto label1; label2: printf( "%d %d\n", cnt, sum ); } 

## Nothing Up My Sleeve!¶

Now for some trickery! We will use a feature of the C preprocessor called Macros to change this code and make it look more like assembly language. One step at a time.

### The #define Macro¶

A Macro is a text substitution trick. You may have seen something like this in C code:

#define MAX 100
...
if(count < MAX) ...


It is important that you realize that MAX is not a “variable”, here. It is a name associated with a pile of characters. The macro associates the name MAX with all of the text beginning with the first non-blank character after the name and continuing all the way to the end of the line.

Warning

Putting extra spaces after the text you want can lead to problems later, watch that. You canot see them in your code, but they might be there!

Here the Preprocessor, which actually reads your code first, will convert any occurrences of MAX to 100 (by substituting that text following the name). When the compiler finally gets the output from the Preprocesser* the name MAX is gone from your code! We will create two macros to hide part of our current code, but when the smoke clears, the compiler will still see exactly what you see now!

## Converting to Pentium Instructions¶

Here are our first macros, placed in a new file we will include named macros.h:

#define CMP( d1, d2 )       flag = ( d1 == d2 )
#define JZ( label )         if( flag ) goto label
#define JMP( lab )          goto lab


Note

Notice that the macro requires the use of parentheses to mark where parameters can be used when you use the macro name. In real assembly language, we will not see parentheses. The actual Pentium line will just read “CMP d1, d2”. That is as close as I can get in this morphing experiment.

Now, we can get rid of those two logical expressions and replace them with something that looks like a Pentium CMP instruction. The CMP is just a code for “compare”. We can also replace the conditional branches with something that looks like a JZ instruction, and the absolute branch with something that looks like a JMP instruction. Again JMP is a code for “absolute jump”, and JZ is a code for “conditional jump” (jump if the result of that last comparison produced a zero result):

  1 2 3 4 5 6 7 8 9 10 11 12 13 int main( void ) { cnt = 0; label1: CMP( cnt, 9 ); JZ( label2 ); CMP( odd, 0 ); JZ( label3 );; sum += data[ cnt ]; label3: odd = !odd; cnt++; printf( "%d %d\n", cnt, sum ); JMP( label1 ); label2: printf( "%d %d\n", cnt, sum ); } 

Now that is nice! We are finally starting to see something more like assembly code in this file!

Note

If you really want to prove that the compiler still sees the same code, you can get gcc to show you what the preprocessor produced by doing this:

$nasm sum1.asm  Wow! ## AVR Assembly Code¶ Your compiler takes a program written in the designated programming language, like “C”, and translates it into another language. That second language is the language of the machine where you intend to run your program. For everything we normally do as developers, that machine is the same on you do your development work on. There is no reason why that second machine has to be the one you intend to run your code on. Suppose you have a tiny machine that is not powerful enough to run a compiler. All you need is a program that will run on your development machine, but this one produces something that can run on your tiny machine. Of course, we will need to find a way to get the output from this compiler onto that tiny machine, but that is often quite simple, as we will see. We call this kind of compiler a cross compiler, and there is a nice one for the real chip we are trying to simulate in our class project. In your class VM do this: $ sudo apt-get install -y gcc-avr


When this command completes, you will have a second version of the exact same compiler you have been running up to now, only the target language of this machine is for the Atmel AVR processor family.

When we get to the point where we want to try to run real programs on our simulator, having this tool will be extremely handy.

## Nasm Version¶

Just for reference, here is a proper Pentium assembly language file, for the Nasm assembler, that will do exactly what our new file says to do.

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 ; simple summer ; compile with: ; nasm -f elf64 -o sum.o sum.asm ; gcc -o sum sum.o ; ./sum ; ; initialized data section -------------------------------- section .data data dd 5,3,7,10,42,6,22,15,32 dptr dq data ; pointer to arrat odd dd 0 sum dd 0 msg1: db "%d", 0 ; integer format string msg2: db " ", 0 ; space msg3: db 0ah, 0 ; newline fmt dd 0 ; pointer to format string pdata dq 0 ; pointer ; uninitialized data section ------------------------------ section .bss cnt resd 1 flag resd 1 ; code section -------------------------------------------- section .text global main extern printf display: mov rdi, msg1 ; rdi has format string address mov rsi, [cnt] ; rsi has value to print mov rax, 0 ; required call printf ; print cnt mov rdi, msg2 mov rax, 0 call printf ; then a spaceDD mov rdi, msg1 mov rsi, [sum] mov rax, 0 call printf ; print sum mov rdi, msg3 mov rax, 0 call printf ; newline ret main: mov eax, 0 ; get a 32 bit zero mov [cnt], eax ; initialize cnt ; l1: mov eax, [cnt] ; get current count cmp eax, 9 ; hit the end yet? jz l3 ; end if so mov eax, [odd] ; fetch current value of odd cmp eax, 0 ; is it zero jz l2; ; mov rax, 0 ; make 64 bit zero mov eax, [cnt] ; load in current count mov ecx, 4 ; multiply by 4 bytes mul ecx mov rbx, [dptr] ; fetch pointer to array add rbx, rax ; add offset to number mov eax, dword [rbx] ; fetch item from array add [sum], eax ; add this one in ; l2: not dword [odd] ; flip the boolean inc dword [cnt] ; bump the counter call display jmp l1 l3: mov rax, 0 ret 

And this is exactly what your tortured C file does as well.

Wow!

Just for fun, let’s see what this code looks like when we let avr-gcc compile it:

\$ avr-gcc -mmcu=attiny85 -S sum.c -o sum-avr.asm


This command tells the compiler to just compile, and dump the assembly language it produced so humans can look it over. This is extremely handy when learning how to write assembly language for some chip! (Or for building a simulator, It tells us what instructions we really need to simulate!)

The result of this, stripped of junk we do not need to worry about here, looks like this;

sum-avr.asm
  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 ; set up global data area ============================================= .data ; set up "data" array data: .word 5 .word 3 .word 7 .word 10 .word 42 .word 6 .word 22 .word 15 .word 32 ; set up uninitialized "cnt" and "sum" variables .comm cnt,2,1 .comm sum,2,1 ; set up initialized variable "odd" .section .bss odd: .zero 2 ; initialize 16-bits with zero ; program code starts here ==================================== .text main: rjmp .L2 .L5: lds r24,odd ; load "odd" into r24,r25 lds r25,odd+1 or r24,r25 ; OR the two bytes (why?) breq .L3 ; branch if equal to zero lds r24,cnt ; load "cnt" into r24,r25 lds r25,cnt+1 lsl r24 ; left shift, high bit into carry flag rol r25 ; rotate left, carry enters low bit (16-bit *2) subi r24,lo8(-(data)) ; 16-bit subtract r24,r25 from (-data[i]) HUH? sbci r25,hi8(-(data)) movw r30,r24 ; save result in "z" (r30,r21) ld r24,Z ; 16-bit move (data[Z] -> r24,r25) ldd r25,Z+1 lds r18,sum ; get current sum into r18,r19 lds r19,sum+1 add r24,r18 ;add lo(sum) + lo(data[i]) adc r25,r19 ; add with carry (hi(sum) + hi(data[i]) sts sum+1,r25 ; put result back in r24,r24 sts sum,r24 .L3: lds r24,odd ; load 16_bit odd into r24,r24 lds r25,odd+1 ldi r18,lo8(1) ; set up 16_bit "1" or r24,r25 ; see if this is zero breq .L4 ; if so, branch ldi r18,0 ; set r18 to zero .L4: mov r24,r18 ; save r18 inro r24 ldi r25,0 ; set r25 to zero sts odd+1,r25 ; save result in "odd" sts odd,r24 lds r24,cnt ; load "cnt" into r24,r25 lds r25,cnt+1 adiw r24,1 ; add 1 to 16-bit value in r24,e25 sts cnt+1,r25 ; save result back in "cnt" sts cnt,r24 .L2: lds r24,cnt ; load 16-bit z'cntz' into r24, r25 lds r25,cnt+1 sbiw r24,9 ; 16-bit subtract "9" from r24,r26 brlt .L5 ; branch if less ; end of program, but where do we go? ================================= ret 

This was an interesting exercise (especially at 3am one morning). Fortunately, I have a lot of experience using the C preprocessor to modify code. This is not a form of programming you should do much of. Obviously, the end result will hide a lot of what is really going on. But, in this case, we are trying to see what the code looks like inside of the machine, and that code is definitely not C!

If you look closely at these last two files, you will see one fundamental difference that I am not going to fix. (You might try that as an exercise). There are several places where I am referencing variables in memory directly. In fact, the assembly language moves some data into internal storage places called registers, and I did not set up any of those in this example. The Pentium processor cannot reference two memory items in the same line. There are instances in this code that clearly need to be fixed. That should be easy enough, but I have accomplished my basic goal in what we see here.

## What Instructions Did We Need¶

It is worthwhile examining exactly what assembly language constructs we needed from all those available in the Pentium to get this code running. Here is a summary:

• CALL - branch to a function
• CMP - see how two data items relate
• INC - increment a data item (very common action)
• JMP - absolute branch
• JZ - conditional branch
• MOV - copy data from one place to another
• MUL - multiply two data items as integers
• NOT - logical compliment of a binary value
• RET - return from a function

There are different forms of a few of these, but that is not many for a moderate C program.

See! Assembly language is not so scary!

The key concept to take away from all of this warping of code is this:

The processor does very simple things. Those things look nothing like the code you originally wrote. Some tool, normally called a compiler, is responsible for translating what you wrote into something the processor can digest. Our final form was not quite that. We introduced a “humanized” form of what the processor can really digest (0s and 1s). We call that form assembly language.

This is done because us humans do not work well with piles of 0s and 1s!. We add in one more tool, called an assembler, to translate assembly language into the processor’s real language.

In fact, the gcc compiler does exactly that:

• It reads your program using the preprocessor to get rid of those lines starting with a “#”
• It then translates the resulting file into assembly language
• Next, it invokes the assembler to translate that code into 0s and 1s.
• Finally, it saves that stuff in an executable file.