AArch64 Playground
4.1 · C in one sitting

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.

TypeBytesHolds
char1one character, or a small integer
short2an integer from -32,768 to 32,767
int4an integer from about -2.1 billion to 2.1 billion
long8an integer up to about 9.2 billion billion either way
float4a number with a fraction, accurate to about 7 digits
double8a number with a fraction, accurate to about 15 digits
any pointer8a 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, so 17 / 5 is 3. It cuts toward zero, so -17 / 5 is -3, not -4.
  • % gives the remainder, what is left over after that division: 17 % 5 is 2, and -17 % 5 is -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;   // 2

printf: 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.

SpecifierPrints
%dan int in decimal
%lda long in decimal
%uan unsigned int
%xan integer in hexadecimal (base 16)
%cone character
%sa string: a run of characters, such as a word or a sentence
%fa 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 ff

scanf: 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 n

Functions 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, &quot, &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 while loop tests its condition first. If the condition is false from the start, the body never runs. This is a pre-test loop.
  • A do ... while loop 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 = 450

note

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 average3 in registers w0, w1 and w2, and the answer comes back in w0.
  • sdiv divides and drops the fraction, like / on two ints.
  • printf is the same C function, called with bl. The address of its format string goes in x0, and the values for the four %d specifiers go in w1 to w4.
#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;}
loading editor...

regfile

N clearZ clearC clearV clear

x0–x30 are the integer registers.

X0arg00x0000000000000000
X1arg10x0000000000000000
X2arg20x0000000000000000
X3arg30x0000000000000000
X4arg40x0000000000000000
X5arg50x0000000000000000
X6arg60x0000000000000000
X7arg70x0000000000000000
X8ind0x0000000000000000
X90x0000000000000000
X100x0000000000000000
X110x0000000000000000
X120x0000000000000000
X130x0000000000000000
X140x0000000000000000
X150x0000000000000000
X16ip00x0000000000000000
X17ip10x0000000000000000
X18pr0x0000000000000000
X190x0000000000000000
X200x0000000000000000
X210x0000000000000000
X220x0000000000000000
X230x0000000000000000
X240x0000000000000000
X250x0000000000000000
X260x0000000000000000
X270x0000000000000000
X280x0000000000000000
X29fp0x0000000000000000
X30lr0x0000000000000000
SP0x0000000080000000
PC0x0000000000400000
console

Output prints here as your program runs.

Press step or run under the editor, or feed stdin from the box below.

not assembled

example 1try it: run it, or step one instruction at a timeOpen in playground

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

  1. How many bytes does an int take on these machines, and how many does a pointer take?
  2. What are 23 / 4 and 23 % 4 in C?
  3. Why does scanf("%d", &n) need the &?
  4. The condition of a do ... while loop is false from the start. How many times does its body run?
  5. An int pointer p holds the address 1000. What address does p + 1 hold?

answers

show answers
  1. 4 bytes for an int and 8 for a pointer.
  2. 5 and 3.
  3. scanf has to store into n, so it needs to know where n lives; a copy of its value would be no use.
  4. Once.
  5. 1004, one int further on.

Practice

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.