This guide is meant to teach how to write a simple hello world program using C on Linux. We will be using C89.
Let's start by creating the entry point to our program in a file called hello.c.
int main(void) {
return 0;
}
Now let's see if it compiles with:
cc -o hello hello.c
Next, we'll prepare the message that we want to print:
int main(void) {
static const char* hello_world = "Hello world\n";
/* ... */
This will prepare a character buffer (a string) which we will then write to stdout. However, we first need to know the length of the string. The following incantation should do the trick:
/* ... */ static unsigned long len = 0; for(;(++len)[hello_world];); /* ... */
Alright. Now that we know the length of our character buffer, we can tell the operating system to write it out to standard output. For that we'll need to execute a syscall. Normally we'd just use inline assembly here as it is common practice, but inline assembly is not actually standardized in C and is usually implemented using compiler extensions, so we will just write our own assembly in a different file instead to keep things portable.
Let's create a file called write.s and create our function.
First the declaration:
.section .text ;; the following resides in the .text section (code)
.global write ;; the label "write" will be a global symbol
write: ;; here's write
Then we'll set up the write syscall. On x8664 linux, the write syscall expects the following register values:
rax- 1
syscall number that corresponds to the
writefunction. other syscalls and their parameters can only be found on this official documentation page provided by google. rdi- file descriptor as a 64 bit unsigned integer We will use the value 1 here because on linux the 1 file descriptor corresponds to stdout of the process
rsi- character pointer Pointer to the start of our character buffer.
rdx- length of the string To tell the os how many characters to print.
Luckily, the C abi for x8664 linux is similar to the syscall API meaning that arguments to a function in C follow the same convention to arguments to syscalls.
mov $1, %rax ;; syscall number mov %rdi, %rdi ;; redundant, but shown here for didactic purposes mov %rsi, %rsi ;; ^ mov %rdx, %rdx ;; ^ syscall ;; Do the thing. ret ;; return from the function
Okay cool. We now have a function to write any string to any file descriptor, so lets use it to print to stdout back home in our C code.
We'll first need to tell C of the existence of this function by providing its signature at the top of the file. Since this is the last step, I'll provide the entire C code here, including the final return call. The return value is used to indicate whether or not the program succesfully completed using 0 to denote sucess and any other integer value to denote failure. In our case, absolutely nothing can fail, so we just return 0.
/* tell c our function exists */
void write(unsigned long fd, const char* buf, unsigned long len);
int main(void) {
static const char* hello_world = "Hello world\n";
static unsigned long len = 0;
for(;(++len)[hello_world];);
write(1, hello_world, len); /* use our write function */
return 0; /* declare victory */
}
Finally, let's compile and run this code with the following command:
cc -o hello hello.c write.s && ./hello
That's it for this one. Hopefully this has opened your eyes to the simplicity and power of C.
Addendum
This section is optional and deals with optimizations and other advanced topics.
Now that we're comfortable writing sophisticated programs in C, let's try our hand at optimizing them.
I will begin by lifting my variables into the global scope. This makes it easy for the CPU to access these variables as it can just access them from the global scope instead of searching through all the local scopes. As I am doing this, I will also tell the CPU that these variables are volatile, which means I am going to try to access them in the future. It helps the CPU cache them earlier.
static const volatile char * const volatile h = "hello world\n";
static volatile unsigned long l = 0;
int main(void) {
/* ... */
You'll notice that I've also shortened the variable names. This just makes the compiler faster, since there's less code to process.
Next, I'll optimize the write function. I will begin by shortening the name, and then I'll make all its parameters volatile. I will also tell the compiler that some of the arguments should be kept in registers since our assembly function really doesn't move them around in memory. This will add a significant boost to performance.
extern int w(const unsigned long volatile register f,
const volatile char register * const volatile b,
const unsigned long volatile register l);
/* ... */
I've also broken the function signature down into multiple lines for readability. It's important to remember to change the write symbol to w in the assembly code too.
The final optimization will be in the main function. First, I'll optimize the signature in the same way, and then I'll omit the return value because the compiler will understand that I am a good C programmer after seeing my optimized code, thereby assuming the program will always succeed. Finally, instead of having the length calculation and printing happening in separate lines, I will do it all in one line to save CPU cycles:
/* ... */
int main(register void) {
for(;l++[h]?1:!w(1,h,l-1););
}
Here's the full code in all its glory:
extern int w(const unsigned long volatile register f,
const volatile char register * const volatile b,
const unsigned long volatile register l);
static const volatile char * const volatile h = "hello world\n";
static volatile unsigned long l = 0;
int main(register void) {
for(;l++[h]?1:!w(1,h,l-1););
}
As fast as this program is, it can sure use some more speed and robustness by making use of all the cutting edge features that come in more modern C editions, and compiler extensions. I am not an expert in that area so I've consulted a friend and they've came up with what I would consider to be a very standard way to do modern a hello world program.
extern // makes it extern
void // makes it void
write
(
unsigned // makes it unsigned
long // makes it long
register // makes it a register
,
const // makes it const
char // makes it a char
register // makes it a register
* // makes it a pointer
restrict // makes it restricted
,
unsigned // makes it unsigned
long // makes it long
register // makes it a register
);
static // makes it static
const // makes it constant
volatile // makes it volatile
char // makes it char
* // makes it a pointer
const // makes it const
volatile // makes it volatile
h = "hello world\n";
static // makes it static
volatile // makes it volatile
unsigned // makes it unsigned
long // makes it long
* // makes it a pointer
volatile
l = &(unsigned long){0UL};
static // makes it static
inline // makes it inline
long // makes it long
w
(
unsigned
long
f
,
const
void
*
restrict
b
,
unsigned
long
l
)
{
long ret;
__asm__ __volatile__ ( "call write" : "=a"(ret) : "D"(f), "S"(b), "d"(l) : "rcx","r8","r9","r10","r11","memory");
return ret;
}
void __main(const char * const * envp)
{
const char *envv = *envp;
do
{
l[0] = 0;
for(;(*l)++[envv];);
w(1, envv, --*l);
w(1, "\n", 1);
}
while
((void)*++envp, envv=*envp);
}
int main(const int argc, const char * const * const argv, const char * const * envp)
{
(void) argc;
(void) argv;
__main(envp);
return 0;
}
I hope you've enjoyed your stay. Good bye.