Outline#

In this week’s lab, you will:

  1. Learn about the C programming toolchain we use in the rest of the course
  2. Learn to write simple C programs
  3. Practice reading inputs and generating outputs from your C programs

Preparation#

Let’s begin.

  1. Log in with your uni ID/password and open the lab template on GitLab here.

  2. Fork the repository by clicking on the fork button. This will create your own copy of the lab repository.

  3. Check the URL bar and make sure it contains your uid (eg. gitlab.cecs.anu.edu.au/*your-uid-here*/comp2310-labs). This means you are looking at your copy of the lab, and not the template. Once you’ve done this, click Clone and copy the address next to “Clone with HTTPS”.

  4. Open a terminal and clone with the Git clone command:
    git clone address_from_step_3_here
    

    Type in your uid and password when requested.

  5. You should now have a local copy of the repo on your computer. Try moving to the repo’s folder with the cd and ls commands from earlier.

Congratulations! If you’ve got to this point unscathed, you’ve successfully forked and cloned. You’ll revisit Git later when we cover the add, commit, and push part of using Git - after you’ve made some changes to your lab.

Take your time to familiarize yourself with Git early in the course - you won’t be able to submit the assessment items any other way. We also have videos about using Git on a youtube channel.

Recommended only for lab machines: If you don’t want to type in your uid and password every time you pull and push, you can set up Secure Shell (SSH) keys that authenticate without the need for usernames and passwords. See this page for details. However, use of SSH keys requires either being on campus or connecting through the GlobalProtect VPN.

Before we begin, the first step is to set up the software we use in this course.

  • If you are using a lab machine, then congratulations - most of the work has been done already!
  • If you are working on your own computer, there are a few different things to install. You can continue reading the lab while the software installs itself. The instructions are on a separate page here.

Introduction#

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 be teaching the x86 ISA in the first few lectures 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.

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#

Since 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 lab1 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. If it worked then you should see a new file hello_world in the root lab1 folder. 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 lab1 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 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. 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

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 lab1]$

you might be forgetting to add newlines

Hello, world!
The program has ended
[user@host lab1]$

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 <the address of x>
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.

pointers 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 <stdio.h> 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 <stdio.h> and <stdlib.h> headers to access the functions declared in those headers, which we can then call from our programs. The <stdio.h> contains the declarations for C standard I/O functions. I/O stands for Input/Output. The <stdlib.h> 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 <stdio.h>

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 has support for C. 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.

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.

  1. 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. 

bars search times arrow-up