Summary
- Overview of how clang and LLVM work together, and where LLVM IR code fits in
- How to use command-line tools to produce LLVM IR from C code, and to run LLVM IR code directly.
- The structure of LLVM IR code: function headers, constants, main function.
- LLVM registers and the SSA concept
- LLVM commands and the 3AC structure
Clang, LLVM, and IR
As you saw in the video from HW 1.1, when you compile a C program using clang with a command like
clang myprog.c -o myprog
there are actually a few steps happening:
- The clang “front-end” translates
myprog.cinto LLVM IR code - The LLVM IR code is optimized
- The optimized LLVM IR is translated into machine code and linked
into the executable
myprog
Other compilers work in similar ways. For example, gcc uses multiple intermediate representation languages on the journey from C source code to a compiled machine code binary; GIMPLE is the one that is probably most similar to LLVM IR.
What makes LLVM a bit special is that they designed LLVM IR to be language-independent, meaning that many different programming languages can all be compiled to LLVM IR. These are called “front-ends”, and it is what you will be writing in this class when you write compilers.
After the front-end converts some programming language to LLVM IR, then the “back-end” kicks in to optimize and produce machine language for whatever architecture you want.
Command-line tools
To convert from a C source program to LLVM IR, you run:
clang -S -emit-llvm myprog.c
which will create a file myprog.ll. This is the
“front-end” pass, converting source code in a normal programming
language to LLVM IR.
The back-end pass, compiling LLVM IR down to an executable file, is just like compiling C itself:
clang myprog.ll -o myprog
(Based on the filename extension .ll, clang knows you are passing in
LLVM IR code instead of C source code.)
(Note, there are actually separate commands opt, llc, llvm-link to
optimize, compile, and link LLVM IR code, but we will not usually need
to get into that.)
An example program
Let’s take the following C program:
include <stdio.h>
include <stdlib.h>
include <unistd.h>
int die_roll() {
return (rand() % 6) + 1;
}
int main() {
srand(getpid());
int x = die_roll();
int y = die_roll();
printf("%d + %d = %d\n", x, y, x+y);
return 0;
}
If you run clang -S -emit-llvm it will produce something like the
following .ll file:
; ModuleID = 'llvm-example.c'
source_filename = "llvm-example.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"
@.str = private unnamed_addr constant [14 x i8] c"%d + %d = %d\0A\00", align 1
; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @die_roll() #0 {
%1 = call i32 @rand() #3
%2 = srem i32 %1, 6
%3 = add nsw i32 %2, 1
ret i32 %3
}
; Function Attrs: nounwind
declare i32 @rand() #1
; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 {
%1 = alloca i32, align 4
%2 = alloca i32, align 4
%3 = alloca i32, align 4
store i32 0, ptr %1, align 4
%4 = call i32 @getpid() #3
call void @srand(i32 noundef %4) #3
%5 = call i32 @die_roll()
store i32 %5, ptr %2, align 4
%6 = call i32 @die_roll()
store i32 %6, ptr %3, align 4
%7 = load i32, ptr %2, align 4
%8 = load i32, ptr %3, align 4
%9 = load i32, ptr %2, align 4
%10 = load i32, ptr %3, align 4
%11 = add nsw i32 %9, %10
%12 = call i32 (ptr, ...) @printf(ptr noundef @.str, i32 noundef %7, i32 noundef %8, i32 noundef %11)
ret i32 0
}
; Function Attrs: nounwind
declare void @srand(i32 noundef) #1
; Function Attrs: nounwind
declare i32 @getpid() #1
declare i32 @printf(ptr noundef, ...) #2
attributes #0 = { noinline nounwind optnone uwtable "frame-pointer"="all" "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
attributes #1 = { nounwind "frame-pointer"="all" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
attributes #2 = { "frame-pointer"="all" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
attributes #3 = { nounwind }
!llvm.module.flags = !{!0, !1, !2, !3, !4}
!llvm.ident = !{!5}
!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{i32 8, !"PIC Level", i32 2}
!2 = !{i32 7, !"PIE Level", i32 2}
!3 = !{i32 7, !"uwtable", i32 2}
!4 = !{i32 7, !"frame-pointer", i32 2}
!5 = !{!"Debian clang version 19.1.7 (3+b1)"}
There’s a lot there, but hopefully you can see what is going on:
-
Find the definition of the
die_rollfunction -
Notice that the single line of C code
return (rand() % 6) + 1is broken into four steps: callingrand(), taking mod by 6 (this function is calledsremin LLVM for “signed remainder”), then adding 1, and finally returning it. -
Find the definition of
main. -
In
main, memory commands are being used to store the variables. Theallocacalls allocate stack space for the variablesxandy. Thenrand_die()is called twice and the results are stored in those memory locations. - The first argument to the
printfcall is@.str. This is the string literal format string"%d + %d = %d\n"from the C source code. - That string itself is defined at the top of the
.llfile as a global constant. Notice crucially that the constant definition uses the hex for the newline character\0Aand also has to include the null byte\00at the end. - There is no equivalent to
#include. Instead, just the standard library functions which are actually used get their own function declarations somewhere in the LLVM IR file.
A lot of the little decorations and attributes in the LLVM IR emitted by clang are there by default and have some meaning, but aren’t strictly necessary for the code to run. And in fact, storing the variables in memory is overkill here since we just have two variables whose values are not modified.
So here is a stripped down version of exactly the same program in LLVM IR:
target triple = "x86_64-pc-linux-gnu"
declare i32 @rand()
declare void @srand(i32)
declare i32 @getpid()
declare i32 @printf(ptr, ...)
@literal1 = constant [14 x i8] c"%d + %d = %d\0A\00"
define i32 @die_roll() {
%temp1 = call i32 @rand()
%temp2 = srem i32 %temp1, 6
%roll = add nsw i32 %temp2, 1
ret i32 %roll
}
define i32 @main() {
%pid = call i32 @getpid()
call void @srand(i32 %pid)
%x = call i32 @die_roll()
%y = call i32 @die_roll()
%xpy = add i32 %x, %y
%ignored = call i32 @printf(ptr @literal1, i32 %x, i32 %y, i32 %xpy)
ret i32 0
}
Here we see the bare essentials:
- The
target tripleline is necessary, to say what kind of CPU this is supposed to eventually get compiled for - Function declarations of any library function calls that will be
used. Notice that the top-level names all start with
@. It’s important to get the names and types of arguments/returns exactly right here! - The usual C types are not present. Instead LLVM IR code specifies
the bit-length of integers. So
i32, a 32-bit integer, corresponds tointin C. Andi8, a 8-bit or single-bite integer, corresponds tochar. - To declare a string literal, you have to say the length and the fact
that each part of it is a char; this is what the
[14 x i8]part means. This is really just declaring an array of length 14. Don’t forget the null byte at the end! - Within a function definition, each line has to assign to a new
register (more on this later). The register names are local to that
function call and have to start with a
%.
You can run either this or the previous .ll version with the lli
command to see that they actually work.
Single static assignment (SSA)
Let’s look again at the @die_roll() function definition. You might
think to just reuse the same register three times, like this:
define i32 @die_roll() {
%roll = call i32 @rand()
%roll = srem i32 %roll, 6
%roll = add nsw i32 %roll, 1
ret i32 %roll
}
This makes sense logically, but if you try to run it with lli or
compile it with clang, you will get an error like multiple definition
of local value %roll.
This is because LLVM IR does not allow register values to be reassigned. Formally, this is called “single static assignment” (SSA), and it’s something which is kind of unique to compiler IR languages. It makes it easier for the compiler later to assign these to actual registers on your CPU, and allows some other kinds of optimizations.
Three address code (3AC)
You will also notice that, much like any assembly language, you can’t combine multiple steps into a single command. Formally, this is called a “three-address code” because each command in LLVM IR typically looks like
%dest = command %src1, %src2
which has a single destination register and two source registers; hence three addresses.
There are actually many different commands built-in to LLVM IR, which
you can peruse on the official documentation
under “Instruction reference”. In many cases, the command is actually
a command name and a type name, like add i32 — this determines the
types of the inputs as well as the type of the output.
But rather than looking at the reference manual, in most cases the
easiest way to figure out “what is the LLVM command to do this thing I
want” is just to write what you want in a small C program, compile that
with clang -S -emit-llvm, and then look at what gets produced in the
.ll file.