The Clang compiler
Clang is a compiler for C-like programming languages, including C, C ++, Objective C/C++, OpenCL C, and CUDA C/C++. It is part of the LLVM project.
Before continuing, let us make sure that Clang has been successfully compiled:
$ ./bin/clang --version
clang version 16.0.3 (https://github.com/llvm/llvm-project.git da3cd333bea572fb10470f610a27f22bcb84b08c)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /home/vedranm/workspace/Development/llvm-project/builddir/./bin
The Target:
line specifies the default target triple containing the processor architecture, the processor sub-architecture (optional), the processor vendor, the operating system, the environment, and the object format (optional). In the absence of additional parameters spoecifying the architecture, we will compile the code for the host processor architecture (x86-64 in our case, also known as AMD64 and Intel EM64T), the host operating system (Linux in our case), and the host environment (GNU). Notice how the host processor vendor is unknown, as Clang compiled for x86-64 can be executed on both Intel and AMD processors.
Creating executable files from the C/C++ source code
Clang can be used for compiling C and C++ source files to executable files. For example, the C source file example1.c
containing:
#include <stdio.h>
int main(void)
{
printf("Hello from C\n");
}
can be compiled to example1
executable file using the clang
command and -o
parameter:
$ ./bin/clang example1.c -o example1
Executing gives:
$ ./example1
Hello from C
Therefore, the compilation and the execution were successful.
Trying to compile the C++ source file example2.cpp
containing:
#include <iostream>
int main()
{
std::cout << "Hello from C++\n";
}
using the analogous command will fail:
$ ./bin/clang example2.cpp -o example2
/usr/bin/ld: /tmp/example-4609c2.o: in function `main':
example2.cpp:(.text+0x6): undefined reference to `std::cout'
/usr/bin/ld: example2.cpp:(.text+0x19): undefined reference to `std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)'
/usr/bin/ld: /tmp/example-4609c2.o: in function `__cxx_global_var_init':
example2.cpp:(.text.startup+0xf): undefined reference to `std::ios_base::Init::Init()'
/usr/bin/ld: example2.cpp:(.text.startup+0x15): undefined reference to `std::ios_base::Init::~Init()'
clang-13: error: linker command failed with exit code 1 (use -v to see invocation)
The failure occurs at the linking stage; Clang treats the the source file language as C-like instead of specifically C++ and therefore does not link the resulting executable to the C++ standard library. This can be done manually by using the -l
parameter:
$ ./bin/clang example2.cpp -o example2 -l stdc++
Although this approach almost always work, the recommended method is to specify C++ as the source file language using the -x
parameter and let Clang take care of linking the standard library:
$ ./bin/clang example2.cpp -o example2 -x c++
Executing example2
shows that the compilation was successful:
$ ./example2
Hello from C++
For convenience, Clang also provides clang++
command which is equivalent to clang -x c++
. Therefore the C++ source file can also be compiled with:
$ ./bin/clang++ example2.cpp -o example2
Assignment
Select the C or the C++ example and modify it to include other code, e.g. value assignments to variables and control flow (if
, for
, or while
). Pay attention to Clang's reporting of errors and warnings during compilation.
Although Clang is an excellent compiler for practical applications (including large applications such as LibreOffice and Linux), from now on we will focus on using Clang to translate the source code into assembly code or the LLVM intermediate representation (IR). In both case, Clang will produce the output code after the specified compilation stage, but will not link it to libraries. Therefore, Clang will not create an executable file as it has done so far and we will focus on studying the results by reading the output instead of executing it.
Compiling the source code into the target machine assembly
When we run Clang with the -S
parameter, it will translate the C/C++ source code into the assembly code (for the x86-64 architecture) and output that code into the file with the same name as the input file and the extension .s
:
$ ./bin/clang example1.c -S
The file example1.s
contains the following assembly code:
.text
.file "example1.c"
.globl main # -- Begin function main
.p2align 4, 0x90
.type main,@function
main: # @main
.cfi_startproc
# %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
movabsq $.L.str, %rdi
movb $0, %al
callq printf
xorl %eax, %eax
popq %rbp
.cfi_def_cfa %rsp, 8
retq
.Lfunc_end0:
.size main, .Lfunc_end0-main
.cfi_endproc
# -- End function
.type .L.str,@object # @.str
.section .rodata.str1.1,"aMS",@progbits,1
.L.str:
.asciz "hello, world\n"
.size .L.str, 14
.ident "clang version 16.0.3 (https://github.com/llvm/llvm-project.git da3cd333bea572fb10470f610a27f22bcb84b08c)"
.section ".note.GNU-stack","",@progbits
.addrsig
.addrsig_sym printf
The output file name can be specified using the -o
parameter. Specifying -
as the name prints the resulting assembly code to the standard output:
$ ./bin/clang example1.c -S -o -
.text
.file "example1.c"
.globl main # -- Begin function main
.p2align 4, 0x90
.type main,@function
main: # @main
.cfi_startproc
# %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
movabsq $.L.str, %rdi
movb $0, %al
callq printf
xorl %eax, %eax
popq %rbp
.cfi_def_cfa %rsp, 8
retq
.Lfunc_end0:
.size main, .Lfunc_end0-main
.cfi_endproc
# -- End function
.type .L.str,@object # @.str
.section .rodata.str1.1,"aMS",@progbits,1
.L.str:
.asciz "hello, world\n"
.size .L.str, 14
.ident "clang version 16.0.3 (https://github.com/llvm/llvm-project.git da3cd333bea572fb10470f610a27f22bcb84b08c)"
.section ".note.GNU-stack","",@progbits
.addrsig
.addrsig_sym printf
Optimization level can be specified by using some variant of the -O
parameter: -O0
, -O1
, -O2
, -O3
, -Ofast
, -Os
, -Oz
, -Og
, -O
, and -O4
. Official documentation says:
-O0
-- Means "no optimization": this level compiles the fastest and generates the most debuggable code.-O1
-- Somewhere between -O0 and -O2.-O2
-- Moderate level of optimization which enables most optimizations.-O3
-- Like-O2
, except that it enables optimizations that take longer to perform or that may generate larger code (in an attempt to make the program run faster).-Ofast
-- Enables all the optimizations from-O3
along with other aggressive optimizations that may violate strict compliance with language standards.-Os
-- Like-O2
with extra optimizations to reduce code size.-Oz
-- Like-Os
(and thus-O2
), but reduces code size further.-Og
-- Like-O1
. In future versions, this option might disable different optimizations in order to improve debuggability.-O
-- Equivalent to-O1
.-O4
and higher -- Currently equivalent to-O3
.
Assignment
Add some dead code of your choice to the example and find out which optimization levels eliminate it.
Other targets can be specified using the -target
parameter. To compile the code for MIPS on GNU/Linux:
$ ./bin/clang example1.c -S -target mips-unknown-linux-gnu
The resulting example1.s
contains:
.text
.abicalls
.option pic0
.section .mdebug.abi32,"",@progbits
.nan legacy
.text
.file "example1.c"
.globl main # -- Begin function main
.p2align 2
.type main,@function
.set nomicromips
.set nomips16
.ent main
main: # @main
.frame $fp,32,$ra
.mask 0xc0000000,-4
.fmask 0x00000000,0
.set noreorder
.set nomacro
.set noat
# %bb.0:
addiu $sp, $sp, -32
sw $ra, 28($sp) # 4-byte Folded Spill
sw $fp, 24($sp) # 4-byte Folded Spill
move $fp, $sp
sw $zero, 20($fp)
lui $1, %hi($.str)
addiu $4, $1, %lo($.str)
jal printf
nop
addiu $2, $zero, 0
move $sp, $fp
lw $fp, 24($sp) # 4-byte Folded Reload
lw $ra, 28($sp) # 4-byte Folded Reload
addiu $sp, $sp, 32
jr $ra
nop
.set at
.set macro
.set reorder
.end main
$func_end0:
.size main, ($func_end0)-main
# -- End function
.type $.str,@object # @.str
.section .rodata.str1.1,"aMS",@progbits,1
$.str:
.asciz "hello, world\n"
.size $.str, 14
.ident "clang version 16.0.3 (https://github.com/llvm/llvm-project.git da3cd333bea572fb10470f610a27f22bcb84b08c)"
.section ".note.GNU-stack","",@progbits
.addrsig
.addrsig_sym printf
.text
Assignment
Find out if the choice of the operating system and the environment affects the resulting assembly code.
Assignment
Find out if this code can be compiled to assembly code for ARM CPUs, RISC-V CPUs, AMD HD2XXX-HD6XXX and GCN GPUs, and WebAssembly.
Finally, we can compile the C++ source code the same way:
$ ./bin/clang example2.cpp -S
The resulting assembly file example2.s
is slightly more complicated:
.text
.file "example2.cpp"
.section .text.startup,"ax",@progbits
.p2align 4, 0x90 # -- Begin function __cxx_global_var_init
.type __cxx_global_var_init,@function
__cxx_global_var_init: # @__cxx_global_var_init
.cfi_startproc
# %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
movabsq $_ZStL8__ioinit, %rdi
callq _ZNSt8ios_base4InitC1Ev
movabsq $_ZNSt8ios_base4InitD1Ev, %rdi
movabsq $_ZStL8__ioinit, %rsi
movabsq $__dso_handle, %rdx
callq __cxa_atexit
popq %rbp
.cfi_def_cfa %rsp, 8
retq
.Lfunc_end0:
.size __cxx_global_var_init, .Lfunc_end0-__cxx_global_var_init
.cfi_endproc
# -- End function
.text
.globl main # -- Begin function main
.p2align 4, 0x90
.type main,@function
main: # @main
.cfi_startproc
# %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
movabsq $_ZSt4cout, %rdi
movabsq $.L.str, %rsi
callq _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
xorl %eax, %eax
popq %rbp
.cfi_def_cfa %rsp, 8
retq
.Lfunc_end1:
.size main, .Lfunc_end1-main
.cfi_endproc
# -- End function
.section .text.startup,"ax",@progbits
.p2align 4, 0x90 # -- Begin function _GLOBAL__sub_I_example2.cpp
.type _GLOBAL__sub_I_example2.cpp,@function
_GLOBAL__sub_I_example2.cpp: # @_GLOBAL__sub_I_example2.cpp
.cfi_startproc
# %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
callq __cxx_global_var_init
popq %rbp
.cfi_def_cfa %rsp, 8
retq
.Lfunc_end2:
.size _GLOBAL__sub_I_example2.cpp, .Lfunc_end2-_GLOBAL__sub_I_example2.cpp
.cfi_endproc
# -- End function
.type _ZStL8__ioinit,@object # @_ZStL8__ioinit
.local _ZStL8__ioinit
.comm _ZStL8__ioinit,1,1
.hidden __dso_handle
.type .L.str,@object # @.str
.section .rodata.str1.1,"aMS",@progbits,1
.L.str:
.asciz "Hello, world!\n"
.size .L.str, 15
.section .init_array,"aw",@init_array
.p2align 3
.quad _GLOBAL__sub_I_example2.cpp
.ident "clang version 16.0.3 (https://github.com/llvm/llvm-project.git da3cd333bea572fb10470f610a27f22bcb84b08c)"
.section ".note.GNU-stack","",@progbits
.addrsig
.addrsig_sym __cxx_global_var_init
.addrsig_sym __cxa_atexit
.addrsig_sym _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
.addrsig_sym _GLOBAL__sub_I_example2.cpp
.addrsig_sym _ZStL8__ioinit
.addrsig_sym __dso_handle
.addrsig_sym _ZSt4cout
Note the presence of the C++ symbol name mangling, e.g. _ZSt4cout
is the mangled name of std::cout
. For demangling the less obvious mangled names, LLVM provides the llvm-cxxfilt symbol name demangler:
$ ./bin/llvm-cxxfilt _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
std::basic_ostream<char, std::char_traits<char> >& std::operator<<<std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
Compiling the source code into the LLVM intermediate representation
Just like assembly, Clang can output the LLVM intermediate representation (IR), which is more convenient for the study of optimization (i.e. analysis and transformation) passes. To produce LLVM IR, we will use -emit-llvm
parameter in addition to the -S
parameter:
$ ./clang example1.c -S -emit-llvm
This will create the example1.ll
file containing:
; ModuleID = 'example1.c'
source_filename = "example1.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"
@.str = private unnamed_addr constant [14 x i8] c"hello, world\0A\00", align 1
; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 {
%1 = alloca i32, align 4
store i32 0, i32* %1, align 4
%2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i64 0, i64 0))
ret i32 0
}
declare dso_local i32 @printf(i8*, ...) #1
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"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
attributes #1 = { "frame-pointer"="all" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
!llvm.module.flags = !{!0, !1, !2}
!llvm.ident = !{!3}
!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{i32 7, !"uwtable", i32 1}
!2 = !{i32 7, !"frame-pointer", i32 2}
!3 = !{!"clang version 16.0.3 (https://github.com/llvm/llvm-project.git da3cd333bea572fb10470f610a27f22bcb84b08c)"}
Assignment
Find out if the LLVM IR differs for different taget processors and, if it does, in what way.
Assignment
Compare the LLVM IR produced by different optimization levels and see if you can observe the optimizations performed.
From now on, we will treat Clang's code generation as a black box and modify only the optimization steps in LLVM. A good starting point to delve into the inner workings of Clang's code generation is the LLVM Target-Independent Code Generator section contained in the the official documentation.
Author: Vedran Miletić