Lab 8: Introduction to C, Part I
Skip navigation Computer Systems & Organisation School of Computing Search query Search ANU web, staff & maps Search current site content Search Menu Search query Search ENGN2219/COMP6719 Lectures Labs Deliverables Resources Practice Search ANU web, staff & maps Search current site content ENGN2219/COMP6719 Lectures Labs Deliverables Resources Practice menu Search query Search Search ENGN2219/COMP6719 Search query Search Labs Lab 1: Introduction Lab 2: Arithmetic and Logic Unit (ALU) Lab 3: State and Registers Lab 4: CPU, Part I: Manual Execution Lab 5: CPU, Part II: Automatic Execution Lab 6: CPU, Part III: Conditional Execution Lab 7: Practicing QuAC Assembly Lab 8: Introduction to C, Part I Related sites Piazza Streams Wattle SoCo Homepage You are here » Labs » Lab 8: Introduction to C, Part I For Windows users, the software setup instructions have been updated with new steps to install the C compiler. See the software setup page for details. Table of Contents Outline Preparation Introduction Exercise 1: The Toolchain Exercise 2: C Introduction Variables Types Functions printf Pointers Arrays C Strings Headers Interlude: The Anatomy of a C Program Program Arguments Exercise 3: What Did You Say? Extension: Debugging Extension: Disassembly Resources Outline In this week’s lab, you will: Learn about the C programming toolchain we use in the rest of the course Learn to write simple C programs Practice reading inputs and generating outputs from your C programs Preparation It is a good idea to ensure you have completed all labs before this one, ideally having: A basic understanding of assembly language programming A solid understanding of how the QuAC CPU works Complete the software setup guide (it has been updated with new instructions for C). Do an upstream pull to get the latest changes to the lab repository. git fetch upstream
git pull --no-ff --no-edit upstream master
Open the lab8 folder in VS Code. Introduction You will not be using the QuAC CPU in any significant capacity from this point in the labs. We hope that designing and building your CPU has provided an enjoyable learning experience and a base of knowledge you can rely upon when dealing with higher-level concepts later in the course. We are now moving up an abstraction level, away from architecture and assembly language to C. Writing complex real-world applications in low-level assembly is tedious. Programmers typically use C or another language (e.g., C/C++, Java, or Python) for writing real-world applications. Each C statement translates to one or more assembly instructions. The C programming language is designed to target a variety of computing hardware and architectures. In almost all desktop computers and laptops, the architecture is x861. We will not teach the x86 ISA in this course. As you will be compiling C code to run on either lab machines or your device, the resulting machine code will be for an x86 processor. In theory, though, we could compile our C programs into QuAC machine code. Most of the exercises in this lab do not involve writing any substantial code but rather figuring out how things work. We advise that you note down insights and observations as you do the exercises below, as the resulting knowledge will be helpful in later assessments. Exercise 1: The Toolchain Whenever you ran an assembly program in Lab 7, the debugger extension automatically assembled your code and loaded it onto the QuAC CPU. Now that you are writing high-level C programs that run on a real CPU, we require a standard system for compiling code: a toolchain. The name is derived from the multiple tools used to get from your program to machine code. For our simple projects, the toolchain consists of three main components: The compiler is responsible for converting each C file into a machine code file known as an object. The linker is responsible for collecting together all of the compiled objects and linking them together into a single executable program. The build system is responsible for ‘gluing’ together the individual components to provide a simple interface for compiling your program. Let us see the toolchain in action. To use the toolchain, first open the C file src/hello_world.c. Have a quick read of the file contents. What do you think this program will do? Open up a new terminal in VS Code (Menu Bar -> Terminal -> New Terminal) and run the command, make hello_world
If you see make: *** No targets specified and no makefile found. Stop.
Then reopen VS Code with the lab8 folder specifically. If you see another error, go back and complete the software setup steps or look over the troubleshooting tips. The above make command will trigger the compiler, building the program. Next run, make clean
This will remove the output of the previous compilation. Open the file Makefile in your lab folder and have a look through. We do not expect you to be able to write a makefile! However, you should be able to identify a few components. Can you find the commands from earlier? A few other lines of interest include: CC specifies the C compiler to use. We’re using the GNU Compiler Collection (GCC), which will automatically use the linker ld as needed. CFLAGS specifies options to the compiler. Later in the course we will modify these options. -march (machine architecture) explicitly tells the compiler what CPU architecture to target. -Wall (warnings all) enables many of the compiler’s warning messages (though not all of them, despite the name). -Wconversion enables a type conversion warning (see the Types section). -g tells the compiler to generate debug symbols. This is extra information bundled into the executable file that is used by a debugger to correlate the machine code to your program files. Quite handy when you are trying to find out where your code is wrong! Run make hello_world
once again to compile your program. The command creates an executable file called hello_world. This time, run your compiled code (a.k.a. the executable) in the terminal with, ./hello_world
The ./ tells the terminal that you want to run an executable file in the current working directory (i.e., the lab folder). If everything has worked correctly, the following message should appear in your terminal, Hello, world!
To speed up the process, you can combine the commands in to one: make hello_world && ./hello_world
Exercise 2: C Introduction The C programming language was developed at Bell Labs by Dennis Ritchie in 1970s. It entered the scene when AT&T company was developing the Unix operating system. It quickly rose to prominence as one of the most popular programming languages, a crown that it still holds today. C is notably the first choice for writing low-level software (called systems software), e.g., the Linux operating system. Systems software interacts closely with the underlying hardware, e.g., for controlling the CPU and memory-resources. High-level user applications (e.g., Spotify) are typically written today in Python and Java. (Digital is written in Java.) The focus of (user) applications is to provide specific functionality to the user and not to control and manage hardware resources. Nevertheless, there remains a high demand for C, particularly in embedded systems and IoT and other highly resource constrained environments. A defining feature of C is that it is closely tied to the underlying hardware. C statements map efficiently to machine instructions. Furthermore, the language is small and simple, leading to efficient code that runs fast and has a small footprint (e.g., the number of instructions). Therefore, C programs are lightweight compared to other programming languages, such as Java and Python, but are still written at a higher level of abstraction than assembly programs. We will learn in coming labs that C programmers still have low-level access to hardware resources and memory in particular. This last aspect of C is both a blessing and a bane. (You will find out!) In this half of the course, we intend to teach you how programs written in C interact with CPU and memory at the microarchitectural level. We do not intend to teach programming practices in general and every tiny detail about C’s syntax. We advise you to use the recommended textbook and numerous external online resources for help (when needed). Every programming language has certain elements critical to writing programs of decent complexity. This tutorial will go through the ones most relevant to the future labs and the second assessment. Most students in this course have previous programming experience with Python, a very different programming language. Think about how C is different from Python as you read the text below. Variables You can declare variables in C, similar to Python. Unlike Python, you need to specify the variable type when you declare the variable. For example, to declare an integer variable named foo, you could write int foo;
Note the ; at the end. A variable declaration in C needs to end with a ;. If you forget one, then do not worry: the compiler will complain to you. But watch out! We have not given a value to this variable yet. Trying to use this variable without giving it a value is undefined behaviour: anything could happen. That is why it is good practice to initialise your variables with a value when you declare them int foo = 0;
The above statement consists of an expression terminated by a semicolon. It assigns the value 0 to foo. Expression statements are the most common and a basic unit of work in C. Now we can safely use foo in another C statement. It is common to update a variable’s value after creating it. To do this, we assign a new value to the variable foo as follows. foo = 5 + (foo * 3);
Note that + (sum) and * (multiply) are arithmetic operators. There is no need to add the type this time: the type is only required when declaring a new variable. The compiler already knows the types of existing variables. Types So what types are there? C has a few built-in types. Some fundamental ones are as follows. Type Meaning char 8 bit signed integer short 16 bit signed integer int 32 bit signed integer long 64 bit signed integer float single-precision real number double double-precision real number You might notice the lack of a boolean type or composite objects such as strings, sets, lists, or arrays. C deals with the same sort of objects that most computers do, namely characters, numbers, and memory addresses. Because C does not have a built-in boolean type, it is typical to use integers, with zero meaning false and non-zero meaning true. We will see strings in C later. You have seen a C string in use, though: the "Hello, world!" text in src/hello_world.c defines a C string. If you are interested in what ‘single-precision’ and ‘double-precision’ refer to, then look at IEEE 754. The IEEE 754 floating-point number specification is used by nearly all modern computers. All of the integer types above can have unsigned prepended to them to make them unsigned integers. For example int x = -3; // x is signed
unsigned int y = 5; // y is unsigned
If you need a refresher on signed and unsigned types, quickly look at the week 1 part 2 lecture. The floating-point types are always signed. The C compiler uses types to know how much space in memory to use for a value and how operations on that value work. For example, we can add two ints fine int x = 1;
int y = 2;
int z = x + y; // 3
But we get possibly unexpected results when adding integer and floating-point types int x = 1;
double f = 2.7;
int z = x + f; // 3
double q = x + f; // 3.7
In the above example, we add x and f in two different contexts: one assigns the result to an int variable, and the other assigns the result to a double variable. The resulting values from the two operations are different. The C standard defines the rules these conversions follow, so correct programs are allowed to do this. However, it is easy to do this accidentally and not realise it, with the mistake causing errors later on in your program. We have added the -Wconversion flag to our compiler flags to make the compiler show a warning when an expression like the one above may result in lost information. In this case, the compiler would show something like src/example.c: In function 'main':
src/example.c:7:15: warning: conversion from 'double' to 'int' may change value [-Wfloat-conversion]
7 | int z = x + f; // 3
| ^
As a general rule, it is safe to add values and store the result to the ‘larger’ type. For example, int i = 1;
long l = 2;
// Warning: conversion from 'long' to 'int' may change value.
// A long is 64 bits, while an int is 32 bits. There are many
// more unique long values than there are possible int values.
int r = i + l;
// No issue: all ints can be converted to longs without data loss.
// For example:
// 0x12345678 -> 0x0000000012345678 (positive)
// 0x87654321 -> 0xFFFFFFFF87654321 (negative)
long s = i + l;
C has additional types for variables, and we will see them later. Read and predict what compiler warnings related to type conversion will be raised by src/types.c. Also, add any other type combinations that you find interesting. Next, compile the program and compare the outcome against your predictions. make types
Functions Modularity demands that large and complicated programs be divided into small parts. Functions break large tasks into smaller ones. Functions encourage code reuse and enable programmers to build on the work of others. To reuse a function written by another programmer, one needs to know the function interface, i.e., its input arguments and return type. Functions can be called from anywhere in the program and from within itself. A function in C is defined like the following. int sum(int a, int b) {
return a + b;
}
The first int is the return type of the function. All functions must declare the type of value they return. A function that does not return anything can use the type void. The next word is the function name. This example uses sum as the function name. Following in parentheses is a list of the parameters the sum function takes. Anyone who wants to use the sum function must provide a value for each parameter listed here. The parameters in the example are two integers, that we have named a and b. You must declare the type of each parameter (similar to variable declarations). Note that function parameters are the names listed in the function’s definition. Function arguments are the real values passed to the function. Parameters are initialized to the values of the arguments supplied. Finally, inside the curly braces {...} is the function body. The code inside the body gets run when the function is called. The example above has a single statement in the function’s body that returns the sum of a and b. There is one function in a C program with a special purpose: the main function. The main function serves as the entry point into the C program. When you execute a compiled C program, the main function is executed first. From within the main function, you can call other functions, which can, in turn, call other functions, and so on, allowing complex programs. One possible signature for main is as follows int main() {
// statements here...
return 0;
}
We will see an alternative signature for the main function and talk about its interface in detail later. Calling a function is done the same way as in Python: int result = sum(5, 3);
Here we are calling sum with the arguments 5 and 3 and storing the result in a newly declared variable result. printf The C library provides several useful functions for programmers. The printf function prints its arguments on the standard output, which by default is the screen. Unlike most functions you will come across in C, the printf function takes a variable number of arguments. The first argument is always the message to print (such as "Hello, world!"). The remaining arguments are data to be substituted into the message. These arguments can be of any type with a corresponding format specifier. The name printf itself is a combination of print and formatted. A format specifier begins with %. For example, %i is the ‘integer format specifier’. If you use this format specifier, then the printf call must have an integer value passed as the next argument. Format specifiers are matched with printf arguments in order from left to right: the first format specifier is expanded to the value of the second printf argument, the second format specifier is expanded to the value of the third printf argument, and so on. A message does not require format specifiers. You can see this in the src/hello_world.c source file, where only the message to be printed is passed. You will see examples of using format specifiers in the following exercises. Common format specifiers are as follows Format Meaning %% Print a single literal %. Necessary to print, e.g., %i literally instead of as a format specifier. All literal % signs should be written like %% in a format string for safety. Note this does not get matched with an argument. %c Print a char value in ASCII %i Print int value in decimal %x Print int value in hexadecimal %li Print long value in decimal %g Print floating-point value %s Print the contents of a char * string (see Arrays) %p Print pointer value (see Pointers) You will also notice that the message often ends with \n. The compiler turns \n into the newline character. If we forget to put a \n at the end, then whatever is printed next will be put on the same line. So if you see things like Hello, world!The program has ended[user@host lab8]$
you might be forgetting to add newlines Hello, world!
The program has ended
[user@host lab8]$
Read src/format_strings.c and make predictions about what will be printed. The last two lines with strings and pointers have not been introduced to you yet, but take a guess anyway. Then build and run the program make format_strings && ./format_strings
Does it print what you expect? Are there any differences? Other format specifiers and even modifiers can be applied to the given format specifiers. This reference page shows you all the possible values, with examples. The %li format specifier in the table above is an example of a modifier: the l modifies the i format to accept long (64-bit) values instead of 32-bit. Pointers Every type in C has a corresponding pointer type. A ‘pointer’ is C’s terminology for a memory address. Recall that memory is divided into a number of byte or word-sized locations. Each such location has an address. C is unique in that it gives the programmer the ability to manipulate both the address and the contents of any memory location. Therefore, if you have a pointer to a variable, you have the memory address of that variable. Recall in assembly the use of labels. Pointers and labels are similar in the sense that both contain a memory address. C pointers are real variables that exist at run-time (i.e., when the program runs). Learn this statement by heart: A pointer is a variable that contains the address of a variable. The syntax for declaring pointer variables is as follows, int *my_pointer;
Note that we have inserted * between the type (int in this case) and the variable name (my_pointer). Here we have declared that my_pointer is a variable whose value is the memory address of some int variable. The * operator has multiple uses in C. We have just seen a second use above for pointer declaration, and recall the first use was for multiplication. But we have not initialised this variable with a value. We cannot use it for anything yet, because that would be undefined behaviour, and correct programs do not have undefined behaviour. We can initialise the pointer variable to point to another variable as follows int x = 5; // x is an int with value 5
// my_pointer is a pointer with the value
int *my_pointer = &x;
Here we have put & before the use of x to get the address of x instead of its value. OK, so now we have a valid pointer pointing to another variable. What can we do with it? Consider the following program. int x = 5;
int *p = &x; // shorter name `p` for our pointer
*p = 7; // store `7` at the memory pointed to by `p`
// what value does `x` have?
It is important to note that we have introduced above a third use of the * operator, which is to dereference a pointer. Dereferencing allows us to read or modify the memory at the location pointed to by the pointer (which, remember, is just a variable that holds a memory address). As we dereferenced p on the left hand side of the assignment, we have used dereferencing to modify the memory pointed to by p. But this memory is of our variable x! We have changed x without using the name x, except for when we initialised p. Read the file src/pointers.c. Predict the values of x and p that will be printed before and after the dereference step. Then run make pointers && ./pointers
What do you see as the outcome? Does it match your prediction? The operator & gives the address of another variable. The operator * is the dereferencing operator; when applied to a pointer, it accesses the object the pointer points to. Arrays An array is a block of consecutive variables or objects of the same type. The declaration below declares an array named marks of 5 integers. int marks[5];
Array subscripts start at zero in C. The elements of the marks array can be accessed as follows: marks[0], marks[1], marks[2], and so forth. A subscript (enclosed in square brackets) can be any integer constant or expression. In general, the notation marks[i] refers to the i-th element of the array. We refer to i as an array subscript (or index) and the process of accessing a specific element of an array as indexing an array. The five integers in the marks array are stored next to each other in memory. If the first element of the marks array is stored at an address 0x00000000, then the next element is found at 0x00000004. The 4-byte increment is because each element of the array is an integer and each integer is four bytes in size. It is possible to initialize an array during declaration. For example, int marks[5] = {19, 10, 8, 17, 9};
You can also initialize an array like this. int marks[] = {19, 10, 8, 17, 9};
Here, we haven’t specified the size. However, the compiler knows its size is 5 as we are initializing it with 5 elements. The assignments below change the values of the first (at index 0) and fourth (at index 3) array element from their initial value. marks[0] = 10;
marks[3] = 11;
There is a strong relationship in C between arrays and pointers. Any operation that can be achieved with indexing can be done with pointers as well. The name of the array is a synonym for the location of the first element. Therefore, a reference to marks[i] can also be written as *(marks+i). The following two C statements are equivalent, int x = marks[1]; // x contains 10
int x = *(marks + 1); // x still contains 10
In evaluating marks[i], the compiler converts it to *(marks+i) immediately; the two forms are equivalent. Recall that a pointer variable contains the address of a memory location. The following C statement declares a pointer named pa and initialises it with the address of the first element of the marks array. int *pa = marks;
Note that pa now points to the first element of the marks array. We show where the pointer pa points to in the figure below. The C programming language allows pointer arithmetic. This means that we can add an integer to the pointer pa and make it point to any other element of the marks array. For example, pa+1 points to the second element of the marks array, i.e., marks[1] (see top of the figure). Note that pa, pa+1, and pa+2 contain the addresses of the array elements and not their contents. These remarks are true regardless of the type and size of variables in the marks array. The meaning of adding 1 to a pointer, and by extension, all pointer arithmetic, is that pa+1 points to the next object, and pa+i points to the i-th object after pa. To obtain the contents of an array element, we need to use the dereferencing (*) operator. Dereferencing is depicted at the bottom of the figure. For instance, *(pa+2) refers to the value stored at the memory address pa+2. Showing the marks array and pointer arithmetic. Each byte in memory has a unique address. Shown above is the starting address of each array element in memory. The array elements are four bytes apart. On the left are shown C expressions for computing the memory addresses (top) and values (bottom). The following two C statement both print the value of the third element of the marks array, printf("marks[2] = %d\n", marks[2]); // prints 8 on the screen
printf("marks[2] = %d\n", *(pa + 2)); // prints 8 on the screen
Consider the following two C statements. Their behavior is equivalent. printf("marks[2] = %d\n", pa[2]);
printf("marks[2] = %d\n", *(pa + 2));
The close relationship between arrays and pointer should now be obvious. Consider the first statement above. We have just indexed the pointer pa to access an element of the array (marks[2])! The compiler knows that we want to access the third integer in the marks array. Similarly, in the second statement, when we add 2 to pa, the result (memory address) points to the third element of the array. Behind the scenes, when we say pa[2], the compiler multiplies the array subscript by 4 because each array element is four bytes in size in this case. Similarly, when we add 1 to a pointer of type int *, the compiler increments the pointer (memory address) by 4 units instead of just 1 because each integer is 4 bytes in size. Make sure the figure above makes perfect sense and talk to your tutor if you feel confused. Finally, the following two statements are equivalent and both assign the address of the first element of the marks array to the pointer variable pa. // assign pa the address of the first element of array
pa = &marks[0];
// array name is a synonym for first element's address
pa = marks;
We often say that arrays decay to pointers. This behavior is especially true when passing arrays to functions as arguments. (We will use arrays as function arguments in the next tutorial.) There is a special operator in C which we would like you to know. The sizeof() operator gives the size of a variable in bytes. The following example assigns the size of the variable one_mark to the variable track_size. int marks[5] = {19, 10, 8, 17, 9};
int one_mark = mark[0];
int track_size = sizeof(one_mark);
Read the src/arrays.c program and predict the outcome. Then run the program and check if your predictions match the actual outcome. The program should print a warning. Make sure you understand what this warning is about. make arrays && ./arrays
C Strings C uses char type to store characters and letters. Unlike Python, which has a dedicated string type, strings in C are just arrays of char values. So the following is a C string char *my_string = "Hello";
When you create a string in this way, the C compiler will automatically write a byte with value 0x00 at the end. The resulting array is called a null-terminated string, and is a method of determining the length of the string without tracking the length in a separate variable. So the above string initialises memory the same in the following way, char my_string[] = { 'H', 'e', 'l', 'l', 'o', '\0' };
Single and double quotes mean different things in C. Single quotes are used to create char values. Double quotes are used to create string values (an array of char values ending with a null (zero) byte). Similar to \n, the sequence \0 is replaced by the C compiler with an actual null byte. It is not possible to modify a string declared using "..." syntax. Trying to do so is undefined behaviour. It might even work on your computer, but fail on someone else’s. Headers Some programs become very large and unmanageable in a single source file. Furthermore, some programs are written to be reused across multiple projects. Copy/pasting code across projects makes it hard to keep code up to date, and thus reusing code inevitably leads to multiple source code files. We need a way to ‘link’ these independent program files together during compilation. The canonical way to support multiple files in C is with header files. These files are given the .h extension, and they define the signatures of all functions exported from the corresponding .c source file. You have already seen a .h file be used in the previous examples: stdio.h is a header file in the C standard library that defines the printf function signature (stdio is short for standard input / output). We write #include to import these signatures into our program, allowing us to use them. Then, when we run the compiler, the linker includes the associated implementation of printf into our compiled program. #include is called a preprocessor directive, which behaves as if you replaced it with the contents of the specified file at the exact same location. We will often include the and headers to access the functions declared in those headers, which we can then call from our programs. The contains the declarations for C standard I/O functions. I/O stands for Input/Output. The header contains utility functions we will use in future labs. Interlude: The Anatomy of a C Program Return now to your src/hello_world.c file. You should now understand the whole file as written. This basic format will remain the same across your C projects. Line by line, the file contains: #include Includes the stdio.h header that allows use of functions within the stdio (part of the C library). This is required for the printf function. int main() {
Is the declaration of the main function, the starting point of your program. int is the return type of the function. printf("Hello, world!\n");
This line calls the printf function, printing the argument to the standard output or stdout, which you see in the terminal. return 0;
This statement ends the function. Returning 0 is the usual way to signal that the program has terminated without error. }
C uses {} braces for code bodies, () for function arguments, and ; for line terminations. Here we are closing the code body of the main function. At this point, you can start writing complete C programs. We encourage you to write a small program or two in src/exercise2.c to test your understanding of pointers, arrays, and strings. You can use the printf function to aid your understanding of what the program is doing. Also try to print the size in bytes of different C variable types using the sizeof() operator. Program Arguments One last topic before we build a simple useful program is program arguments. This is a list of values passed to our program when it is run. You have used this when you ran the make command: make hello_world
runs the program make with the argument hello_world. That is how make knows to build that particular file. In C, we can get the program arguments from the argc and argv parameters to main. The function signature for main now looks like int main(int argc, char **argv) {
// statements here...
return 0;
}
The type of the char **argv parameter looks new, but you have seen this type before: it is just two levels of pointer! argv is a pointer to a pointer to a char. If you read the memory pointed to by argv, the value you get is itself a pointer of type char *. If you read the memory where that pointer points, then you get a char value. But these argv related pointers are not just to single values — both levels of pointer are to arrays of values. So really argv is an array of C strings, which themselves are null-terminated arrays of char values. Also remember that C arrays do not store their length like you might be accustomed to in Python. This is fine for the C strings, because they end when we see a null byte, but how long is the overall argv array? This is where argc comes in: argc is the number of elements in the argv array. The given names are actually abbreviations: argc: argument count argv: argument vector The term vector in the context of programming refers to a resizable array-like structure. The use of ‘vector’ in the name argv is historic; as you cannot change its size, array is a better description of the actual data structure. So altogether we have an array of string arguments argv, this array having length argc, and each element in the array is a pointer to a null-terminated C string. Exercise 3: What Did You Say? In src/exercise3.c, write a program that, assuming there is at least one program argument, On the first line prints There are X arguments where X is replaced with the number of program arguments On the next line prints the contents of the first (index 0) program argument On the next line prints the contents of the middle program argument On the next line prints the contents of the last program argument Refer back to earlier sections (as needed) for using pointer values and printing formatted strings. Build and run your program with different arguments. E.g., make exercise3 && ./exercise3 first second last
Do you notice something odd? Extension: Debugging So far you have only ran your programs in the terminal, using printf statements to gain insights in to what is happening. Fortunately, VS Code’s debugger works similarly for C as it has for your QuAC CPU in earlier labs. More features of the debugger will also now be available for use. To get started, open the ‘Run and Debug’ panel and pick the program you want to debug in the drop-down menu. Press the play button, and VS Code will build and run your code through the debugger. Extension: Disassembly At times in the C labs, you may need to view the compiled assembly for your program. x86 is a very different instruction set to QuAC — reading through it will likely not provide you with any meaningful information. Instead, you should use Godbolt, an online compiler tool. As shown above, you can select the C language as an input and ask the compiler to generate ARM assembly. QuAC has purposely been designed to look similar to ARM, which is commonly used in mobile devices. Make sure you set the compiler to armv7 or similar. If there is any confusion, or you do not recognise an ARM instruction, please ask a tutor for clarification as they are fluent in both ARM and QuAC assembly. Resources cppreference: Comprehensive C reference pages. This reference is not a tutorial. It is instead a reference for the entire C language. It contains a lot of content and terminology we do not cover. It is most helpful for reviewing documentation and usage of specific functions, such as printf. codecademy: Free external resource for learning C. x86 is a CISC ISA that originally debuted on the Intel 8086 in 1978. It has since been expanded from 16 to 32, to 64 bits. Featuring variable instruction lengths, over 30 extensions, many addressing modes, and full backward compatibility (16-bit code will run on a modern 64bit processor!), x86 is rightfully considered one of the most complex ISAs to exist. Due to this complexity, we instead used QuAC as a teaching architecture in the first half of this course. ↩ Updated: 25 Apr 2022 / Responsible Officer: Director, School of Computing / Page Contact: Shoaib Akram Contact ANU Copyright Disclaimer Privacy Freedom of Information +61 2 6125 5111 The Australian National University, Canberra CRICOS Provider : 00120C ABN : 52 234 063 906 You appear to be using Internet Explorer 7, or have compatibility view turned on. Your browser is not supported by ANU web styles. » Learn how to fix this » Ignore this warning in future