C in one sitting
Many lessons on this site start from a few lines of C and then write the same thing in assembly. This lesson is the C you need to read those lines. It is not a full C course: it covers only the pieces the assembly lessons lean on, in the order they come up.
C does not run on this site; only assembly does. Read the C blocks here, or copy one into a file and build it with gcc on any Linux or Mac machine. The one runnable program, near the end, is assembly.
Types and their sizes
A variable is a named place in memory that holds a value. Every variable in C has a type, which says what kind of value it holds and how many bytes of memory it takes. A byte is 8 bits, and a bit is a single 0 or 1. The sizes below are the ones on the 64-bit ARM Linux machines this site imitates. Assembly cares about them because each size has its own load and store instructions.
| Type | Bytes | Holds |
|---|---|---|
char | 1 | one character, or a small integer |
short | 2 | an integer from -32,768 to 32,767 |
int | 4 | an integer from about -2.1 billion to 2.1 billion |
long | 8 | an integer up to about 9.2 billion billion either way |
float | 4 | a number with a fraction, accurate to about 7 digits |
double | 8 | a number with a fraction, accurate to about 15 digits |
| any pointer | 8 | a memory address |
Writing unsigned in front of an integer type drops the negative numbers and doubles the top of the range: an unsigned int holds 0 to about 4.3 billion.
Dividing whole numbers
C has the usual +, - and *. Two operators behave differently from a calculator:
/on two integers gives a whole number and throws the fraction away, so17 / 5is3. It cuts toward zero, so-17 / 5is-3, not-4.%gives the remainder, what is left over after that division:17 % 5is2, and-17 % 5is-2.
Both rules come back in assembly, where the divide instruction drops the fraction the same way and there is no remainder instruction at all.
int apples = 17;int bags = 5;int per_bag = apples / bags; // 3int left_over = apples % bags; // 2printf: printing with a format string
printf prints text. Its first argument, the first value handed to it, is a format string: the text to print, with a placeholder wherever a value should appear. Each placeholder is a format specifier, a % followed by a letter or two that says how to print the next argument. The arguments after the format string fill the placeholders from left to right.
| Specifier | Prints |
|---|---|
%d | an int in decimal |
%ld | a long in decimal |
%u | an unsigned int |
%x | an integer in hexadecimal (base 16) |
%c | one character |
%s | a string: a run of characters, such as a word or a sentence |
%f | a double; a float is turned into a double on the way in |
\n inside the string is a newline. printf never adds one on its own.
int age = 19;char grade = 'A';printf("age %d, grade %c\n", age, grade); // age 19, grade Aprintf("%d in hex is %x\n", 255, 255); // 255 in hex is ffscanf: reading into a variable
scanf works the other way round: it reads what is typed at the keyboard, turns it into a value, and stores that value in a variable. It reads whole numbers with the same %d and %ld specifiers, with one difference that matters later. scanf is given the address of each variable, written with &, because it has to change the variable, not just read it. The next two sections show why an address makes that possible.
int n;printf("How many? ");scanf("%d", &n); // &n is the address of nFunctions get copies of their arguments
A function is a named piece of code that takes arguments and can hand back, or return, one value. When a function is called, each argument is copied into the function's own parameter, so changing the parameter changes only the copy. This is called passing by value.
void try_to_reset(int x) { x = 0; // changes the copy only}int main(void) { int score = 12; try_to_reset(score); printf("%d\n", score); // still 12 return 0;}Pointers: handing over an address
A pointer is a variable that holds a memory address. &score gives the address of score, and a variable declared as int *p can hold it. A * in front of a pointer, as in *p, means the value stored at that address, so *p = 0 writes into score itself.
This is how a function changes its caller's variables, and how it hands back more than one answer. divmod below returns nothing. It writes its two results through the two addresses it is given. Passing an address this way is called passing by reference.
void divmod(int a, int b, int *q, int *r) { *q = a / b; // write into the caller's quot *r = a % b; // write into the caller's rem}int main(void) { int quot, rem; divmod(38, 7, ", &rem); printf("38 = 7 * %d + %d\n", quot, rem); // 38 = 7 * 5 + 3 return 0;}note
In assembly an address is a 64-bit number in an x register. A pointer argument arrives in a register like any other argument, and the function stores to that address. The scanf call above does exactly this with &n.
Making a choice with if and else
if runs a block only when its condition is true, and else gives the block to run otherwise. A condition compares two values with ==, !=, <, <=, > or >=. C also has switch, a many-way choice on one integer; assembly builds it from the same comparisons.
if (temp < 0) { printf("freezing\n");} else { printf("above freezing\n");}Two loop shapes
A loop repeats a block. C has two basic shapes, and the assembly lessons build both:
- A
whileloop tests its condition first. If the condition is false from the start, the body never runs. This is a pre-test loop. - A
do ... whileloop runs the body first and tests afterwards, so the body always runs at least once. This is a post-test loop.
A for loop is a while loop with its three parts (the start, the test and the step) written on one line.
int i = 1;while (i <= 3) { // test first: may run zero times printf("%d ", i); // prints 1 2 3 i++;}int j = 10;do { // body first: runs at least once printf("%d ", j); // prints 10, once j++;} while (j <= 3);for (int k = 1; k <= 3; k++) { // the same loop as the while above printf("%d ", k);}Arrays, and walking them with a pointer
An array is a row of values of one type, side by side in memory. int marks[6] sets aside 6 ints, 24 bytes in a row. marks[0] is the first element and marks[5] the last; there is no marks[6].
The name of an array, used on its own, is the address of element 0. Adding 1 to a pointer moves it forward by one element, not one byte: for an int pointer that is 4 bytes. So marks[i] and *(marks + i) mean the same thing. The two loops below add up the same array, first by index and then by moving a pointer along it.
int marks[6] = {70, 85, 62, 91, 78, 64};int total = 0;for (int i = 0; i < 6; i++) { // by index total += marks[i];}total = 0;int *p = marks; // p = &marks[0]int *end = marks + 6; // one past the last elementwhile (p < end) { // by pointer total += *p; p++; // moves 4 bytes: one int}printf("total = %d\n", total); // total = 450note
main can take two parameters, int argc and char *argv[]: how many words were typed on the command line, and an array of pointers to those words. Each word is a string, and a string is itself an array of char, so argv is a pointer to pointers. Strings, pointer arrays, and argv walks it in assembly.
A first look at the assembly
Here is a small C program and the same program in assembly. The next lessons explain every kind of line, so do not worry about each one yet. For now, look for three things that carry straight over from C:
- The three arguments travel to
average3in registersw0,w1andw2, and the answer comes back inw0. sdivdivides and drops the fraction, like/on two ints.printfis the same C function, called withbl. The address of its format string goes inx0, and the values for the four%dspecifiers go inw1tow4.
#include <stdio.h>int average3(int a, int b, int c) { return (a + b + c) / 3;}int main(void) { int s1 = 10, s2 = 20, s3 = 25; printf("average of %d, %d and %d is %d\n", s1, s2, s3, average3(s1, s2, s3)); return 0;}Run it. It prints average of 10, 20 and 25 is 18: the true average is 18.33, and integer division drops the .33. Change the three mov lines in main and run it again.
note
main keeps its values in w19 to w22 because a function it calls, such as printf or average3, is free to change x0 to x18, while x19 to x28 keep their values across a call. main uses them here without saving them first, as course programs do in main. Any other function you write must save the ones it uses from x19 to x28 and put them back before it returns.
Check yourself
- How many bytes does an
inttake on these machines, and how many does a pointer take? - What are
23 / 4and23 % 4in C? - Why does
scanf("%d", &n)need the&? - The condition of a
do ... whileloop is false from the start. How many times does its body run? - An
intpointerpholds the address 1000. What address doesp + 1hold?
answers
show answers
- 4 bytes for an
intand 8 for a pointer. - 5 and 3.
scanfhas to store inton, so it needs to know wherenlives; a copy of its value would be no use.- Once.
- 1004, one
intfurther on.
Practice
- Basic quiz: C: type sizes, integer division, copies against addresses, and where an array element sits.
- Sum from one to n: the counting
whileloop above, written in assembly. It needs Pre-test loops first.
Only assembly runs on this site, so every coding exercise is assembly, and many of them start from C like the pieces above. The next lesson is System architecture: what is inside the box.