Guide To Building Applications
Guide To Building Applications
Do you have an open source story to tell? Submit a story idea at opensource.com/story
Email us at [email protected]
Table of Contents
Anyone can compile open source code in these three simple steps...............................................4
A programmer's guide to GNU C Compiler........................................................................................12
How dynamic linking for modular libraries works on Linux...............................................................19
Dynamically linking libraries while compiling code............................................................................25
How to handle dynamic and static libraries in Linux.........................................................................29
How static linking works on Linux........................................................................................................36
32-bit life support: Cross-compiling with GCC..................................................................................41
Jayashree Huttanagoudar
Steven Avenwedde
Stephan is a technology enthusiast who appreciates
open source for the deep insight of how things work.
Stephan works as a full time support engineer in the
mostly proprietary area of industrial automation
software. If possible, he works on his Python-based open
source projects, writing articles, or driving motorbike.
Seth Kenlon
Seth Kenlon is a UNIX geek and free culture advocate. He has worked in the film and
computing industry, often at the same time. He is a writer, editor, and community advocate
for Opensource.com.
By Seth Kenlon
There are many ways to install software, but you get an option not available elsewhere with
open source: You can compile the code yourself. This is the classic three-step process to
compile source code:
$ ./configure
$ make
$ sudo make install
Thanks to these commands, you might be surprised to find that you don't need to know how
to write or even read code to compile it.
#include <iostream>
int main() {
cout << "hello world";
}
—into machine language, the instructions that a CPU uses to process information. You can
look at machine code, but it wouldn't make any sense to you (unless you're a CPU).
With your system set up, there are a few tasks that you'll repeat each time you want to
compile your software:
You have all the commands you need, so now you need some software to compile.
For this exercise, I'm using Angband, an open source (GPLv2) ASCII dungeon crawler. It's a
simple application with just enough complications to demonstrate what you need to consider
when compiling software for yourself.
Once you've unarchived it, change the directory into the extracted directory and have a look
around. There's usually a README file at the top level of the directory. This file, ideally,
contains guidance on what you need to do to compile the code. The README often contains
information on these important aspects of the code:
• Language: What language the code is in (for instance, C, C++, Rust, Python).
• Dependencies: What other software you need to have installed on your system for
this application to build and run.
• Instructions: The literal steps you need to take to build the software. Occasionally,
they include this information within a dedicated file intuitively entitled INSTALL.
If the README file doesn't contain that information, consider filing a bug report with the
developer. You're not the only one who needs an introduction to source code. Regardless of
how experienced they are, everyone is new to source code they've never seen before, and
documentation is important!
Angband's maintainers link to online instructions to describe how to compile the code. This
document also describes what other software you need to have installed, although it doesn't
exactly spell it out. The site says, "There are several different front ends that you can
optionally build (GCU, SDL, SDL2, and X11) using arguments to configure such as --enable-
sdl, --disable-x11, etc." This may mean something to you or look like a foreign language,
but this is the kind of stuff you get used to after compiling code frequently. Whether or not
you understand what X11 or SDL2 is, they're both requirements that you see pretty often after
regularly compiling code over a few months. You get comfortable with the idea that most
software needs other software libraries because they build upon other technologies. In this
case, though, Angband is very flexible and compiles with or without these optional
dependencies, so for now, you can pretend that there are no additional dependencies.
$ ./configure
$ make
$ sudo make install
Those are the steps for projects built with Autotools, which is a framework created to
standardize how source code is delivered. Other frameworks (such as Cmake) exist, however,
and they require different steps. When projects stray from Autotools or Cmake, they tend to
warn you in the README file.
Configure
Angband uses Autotools, so it's time to compile code! In the Angband directory, first, run the
configuration script included with the source:
$ ./configure
This step scans your system to find the dependencies that Angband requires to build
correctly. Some dependencies are so basic that your computer wouldn't be running without
them, while others are specialized. At the end of the process, the script gives you a report on
what it has found:
[...]
configure: creating ./config.status
config.status: creating mk/buildsys.mk
config.status: creating mk/extra.mk
config.status: creating src/autoconf.h
Configuration:
-- Frontends --
- Curses Yes
Some of that output may make sense to you, some of it may not. Either way, you probably
notice that SDL2 and SDL are marked as Disabled, and both Test and Stats are marked with
No. Although negative, this isn't necessarily a bad thing. This, essentially, is the difference
between a Warning and an Error. Had the configure script encountered something that
would prevent it from building the code, it would have alerted you with an error.
If you want to optimize your build a little, you can choose to resolve these negative messages.
By searching through the Angband documentation, you might decide that Test and Stats
aren't actually of interest to you (they're developer options, specific to Angband). However,
with a little online research, you might discover that SDL2 would be a nice feature to have.
To resolve a dependency when compiling code, you need to install the missing component
and the development libraries for that missing component. In other words, Angband needs
SDL2 to play sound, but it needs SDL2-devel (called libsdl2-dev, on Debian systems) to
build. Install both with your package manager:
$ ./configure --enable-sdl2
[...]
Configuration:
[...]
- Curses Yes
- X11 Yes
- SDL2 Yes
- SDL Disabled
- Windows Disabled
- Test No
- Stats No
Make
Once everything's configured, run the make command:
$ make
This usually takes a while, but it provides lots of visual feedback, so you'll know code is getting
compiled.
Install
The final step is to install the code you've just compiled. There's nothing magical about
installing code. All that happens is that lots of files get copied to very specific
directories. That's true whether you're compiling from source code or running a fancy
graphical install wizard. Because the code is getting copied to system-level directories, you
must have root (administrative) privileges, which get granted by the sudo command.
$ angband
Compiling code
I compile most of my own applications, whether on my Slackware desktop computer or my
CentOS laptop using NetBSD's pkgsrc system. I find that by compiling software myself, I can
be as particular as I want to be about the features included in the application, how it's
configured, which library version it uses, and so on. It's rewarding, and it helps me keep up to
date with new releases and, because I sometimes find bugs in the process, it helps me get
involved with lots of different open source projects.
It's rare that you have no other option but to compile software. Most open source projects
provide both the source code (that's why it's called "open source") and installable packages.
Compiling from source code is a choice you get to make for yourself, maybe because you
want new features not yet available in the latest release or just because you prefer to compile
code yourself.
$ mkdir build
$ cd build
$ cmake ..
$ make
$ sudo make install
You can also try compiling with the LLVM compiler instead of the GNU C compiler. For now, I'll
leave that as an exercise for you to investigate on your own (hint: try setting the CC
environment variable).
Once you finish exploring the source code of Angband and at least a few of its dungeons
(you've earned some downtime), have a look at some other codebases. Many will use
Autotools or Cmake, while others may use something different. See what you can build!
By Jayashree Huttanagoudar
A very common compiler is GCC (GNU C Compiler). The compilation process involves several
intermediate steps and adjacent tools.
Install GCC
To confirm whether GCC is already installed on your system, use the gcc command:
$ gcc --version
If necessary, install GCC using your packaging manager. On Fedora-based systems, use dnf:
After installation, if you want to check where GCC is installed, then use:
$ whereis gcc
// hellogcc.c
#include <stdio.h>
int main() {
printf("Hello, GCC!\n");
return 0;
}
$ ls
hellogcc.c
$ gcc hellogcc.c
$ ls -1
a.out
hellogcc.c
As you can see, a.out is the default executable generated as a result of compilation. To see
the output of your newly compiled application, just run it as you would any local binary:
$ ./a.out
Hello, GCC!
$ ls
a.out hellogcc hellogcc.c
$ ./hellogcc
Hello, GCC!
2. Compilation: During this stage, the compiler converts pre-processed source code into
assembly code for a specific CPU architecture. The resulting assembly file is named
with a .s extension, such as hellogcc.s in this example.
3. Assembly: The assembler (as) converts the assembly code into machine code in an
object file, such as hellogcc.o.
4. Linking: The linker (ld) links the object code with the library code to produce an
executable file, such as hellogcc.
When running GCC, use the -v option to see each step in detail.
$ rm a.out hellogcc.o
$ ls
hellogcc.c
Pre-processor
First, start the pre-processor, redirecting its output to hellogcc.i:
$ ls
hellogcc.c hellogcc.i
Take a look at the output file and notice how the pre-processor has included the headers and
expanded the macros.
Compiler
Now you can compile the code into assembly. Use the -S option to set GCC just to produce
assembly code.
$ gcc -S hellogcc.i
$ ls
hellogcc.c hellogcc.i hellogcc.s
$ cat hellogcc.s
$ as -o hellogcc.o hellogcc.s
$ ls
hellogcc.c hellogcc.i hellogcc.o hellogcc.s
Linking
To produce an executable file, you must link the object file to the libraries it depends on. This
isn't quite as easy as the previous steps, but it's educational:
$ ld -o hellogcc hellogcc.o
ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000
ld: hellogcc.o: in function `main`:
hellogcc.c:(.text+0xa): undefined reference to `puts'
An error referencing an undefined puts occurs after the linker is done looking at the
libc.so library. You must find suitable linker options to link the required libraries to resolve
this. This is no small feat, and it's dependent on how your system is laid out.
When linking, you must link code to core runtime (CRT) objects, a set of subroutines that help
binary executables launch. The linker also needs to know where to find important system
libraries, including libc and libgcc, notably within special start and end instructions. These
instructions can be delimited by the --start-group and --end-group options or using
paths to crtbegin.o and crtend.o.
This example uses paths as they appear on a RHEL 8 install, so you may need to adapt the
paths depending on your system.
$ ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 \
-o hello /usr/lib64/crt1.o /usr/lib64/crti.o \
--start-group -L/usr/lib/gcc/x86_64-redhat-linux/8 \
-L/usr/lib64 -L/lib64 hello.o -lgcc \
--as-needed -lgcc_s \
--no-as-needed -lc -lgcc \
--end-group
/usr/lib64/crtn.o
$ ./hello
Hello, GCC!
$ file hellogcc.c
hellogcc.c: C source, ASCII text
$ file hellogcc.o
hellogcc.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
$ file hellogcc
hellogcc: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically
linked, interpreter /lib64/ld-linux-x86-64.so.2,
BuildID[sha1]=bb76b241d7d00871806e9fa5e814fee276d5bd1a, for GNU/Linux 3.2.0, not
stripped
The use the nm utility to list symbol tables for object files:
$ nm hellogcc.o
0000000000000000 T main
U puts
$ ldd hellogcc
linux-vdso.so.1 (0x00007ffe3bdd7000)
libc.so.6 => /lib64/libc.so.6 (0x00007f223395e000)
/lib64/ld-linux-x86-64.so.2 (0x00007f2233b7e000)
By Jayashree Huttanagoudar
When you write an application using the C programming language, your code usually has
multiple source files.
Ultimately, these files must be compiled into a single executable. You can do this by creating
either static or dynamic libraries (the latter are also referred to as shared libraries). These two
types of libraries vary in how they are created and linked. Both have advantages and
disadvantages, depending on your use case.
Dynamic linking is the most common method, especially on Linux systems. Dynamic linking
keeps libraries modular, so just one library can be shared between any number of applications.
Modularity also allows a shared library to be updated independently of the applications that
rely upon it.
Linker
A linker is a command that combines several pieces of a program together and reorganizes
the memory allocation for them.
Create add.c, sub.c , mult.c and divi.c with these function definitions. I'm placing all of
the code in one code block, so divide it up among four files, as indicated in the comments:
// add.c
int add(int a, int b){
return (a+b);
}
//sub.c
int sub(int a, int b){
return (a-b);
}
//mult.c
int mult(int a, int b){
return (a*b);
}
//divi.c
int divi(int a, int b){
return (a/b);
}
Now generate object files add.o, sub.o, mult.o, and divi.o using GCC:
The -c option skips the linking step and creates only object files.
In addition to being sharable, another advantage of a dynamic library is that it reduces the size
of the final executable file. Instead of having a redundant copy of the library, an application
using a library includes only the name of the library when the final executable is created.
You can create dynamic libraries from your existing sample code:
The option -fPIC tells GCC to generate position-independent code (PIC). The -Wall option
isn't necessary and has nothing to do with how the code is compiling. Still, it's a valuable
option because it enables compiler warnings, which can be helpful when troubleshooting.
You have now created a simple example math library, libmymath.so, which you can use in C
code. There are, of course, very complex C libraries out there, and this is the process their
developers use to generate the final product that you or I install for use in C code.
Next, you can use your new math library in some custom code, then link it.
int main()
{
int x, y;
printf("Enter two numbers\n");
scanf("%d%d",&x,&y);
printf("\n%d + %d = %d", x, y, add(x, y));
printf("\n%d - %d = %d", x, y, sub(x, y));
printf("\n%d * %d = %d", x, y, mult(x, y));
if(y==0){
printf("\nDenominator is zero so can't perform division\n");
exit(0);
}else{
printf("\n%d / %d = %d\n", x, y, divi(x, y));
return 0;
}
}
Notice that the first line is an include statement referencing, by name, your own
libmymath library. To use a shared library, you must have it installed. If you don't install the
library you use, then when your executable runs and searches for the included library, it won't
be able to find it. Should you need to compile code without installing a library to a known
directory, there are ways to override default settings. For general use, however, it's expected
that libraries exist in known locations, so that's what I'm demonstrating here.
Copy the file libmymath.so to a standard system directory, such as /usr/lib64, and then
run ldconfig. The ldconfig command creates the required links and cache to the most
recent shared libraries found in the standard library directories.
$ gcc -I . -c mathDemo.c
GCC finds libmymath.so because it exists in a default system library directory. Use ldd to
verify the shared libraries used:
$ ldd mathDemo
linux-vdso.so.1 (0x00007fffe6a30000)
libmymath.so => /usr/lib64/libmymath.so (0x00007fe4d4d33000)
libc.so.6 => /lib64/libc.so.6 (0x00007fe4d4b29000)
/lib64/ld-linux-x86-64.so.2 (0x00007fe4d4d4e000)
$ du ./mathDynamic
24 ./mathDynamic
It's a small application, of course, and the amount of disk space it occupies reflects that. For
comparison, a statically linked version of the same code is 932K!
$ ./mathDynamic
Enter two numbers
25
5
25 + 5 = 30
25 - 5 = 20
25 * 5 = 125
25 / 5 = 5
You can verify that it's dynamically linked with the file command:
$ file ./mathDynamic
./mathDynamic: ELF 64-bit LSB executable, x86-64,
dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2,
with debug_info, not stripped
Success!
By Seth Kenlon
Compiling software is something that developers do a lot, and in open source some users
even choose to do it themselves. Linux podcaster Dann Washko calls source code the
"universal package format" because it contains all the components necessary to make an
application run on any platform. Of course, not all source code is written for all systems, so it's
only "universal" within the subset of targeted systems, but the point is that source code is
extremely flexible. With open source, you can decide how code is compiled and run.
When you're compiling code, you're usually dealing with multiple source files. Developers tend
to keep different classes or modules in separate files so that they can be maintained
separately, and possibly even used by different projects. But when you're compiling these
files, many of them get compiled into a single executable.
This is usually done by creating shared libraries, and then dynamically linking back to them
from the executable. This keeps the executable small by keeping modular functions external,
and ensures that libraries can be updated independently of the applications that use them.
For example, suppose you've written a library called libexample.so, and you want to use it
when compiling your application demo.c. First, create an object file from demo.c:
The -I option adds a directory to GCC's search path for header files. In this example, I
assume that custom header files are in a local directory called include. The -c option
prevents GCC from running a linker, because this task is only to create an object file. And
that's exactly what happens:
$ ls
demo.o include/ lib/ src/
Now you can use the -L option to set a path for your library, and compile:
Notice that the -L option comes before the -l option. This is significant, because if -L hasn't
been added to GCC's search path before you tell GCC to look for a non-default library, GCC
won't know to search in your custom location. The compilation succeeds as expected, but
there's a problem when you attempt to run it:
$ ./myDemo
./myDemo: error while loading shared libraries:
libexample.so: cannot open shared object file:
No such file or directory
$ ldd ./myDemo
linux-vdso.so.1 (0x00007ffe151df000)
libexample.so => not found
libc.so.6 => /lib64/libc.so.6 (0x00007f514b60a000)
/lib64/ld-linux-x86-64.so.2 (0x00007f514b839000)
You already knew that libexample couldn't be located, but the ldd output at least affirms
what's expected from a working library. For instance, libc.so.6 has been located, and ldd
displays its full path.
LD_LIBRARY_PATH
The LD_LIBRARY_PATH environment variable defines the path to libraries. If you're running
an application that relies on a library that's not installed to a standard directory, you can add
to the system's library search path using LD_LIBRARY_PATH.
There are several ways to set environment variables, but the most flexible is to place them
before you run a command. Look at what setting LD_LIBRARY_PATH does for the ldd
command when it's analyzing a "broken" executable:
$ LD_LIBRARY_PATH=`pwd`/lib ldd ./
linux-vdso.so.1 (0x00007ffe515bb000)
libexample.so => /tmp/Demo/lib/libexample.so (0x0000...
libc.so.6 => /lib64/libc.so.6 (0x00007eff037ee000)
/lib64/ld-linux-x86-64.so.2 (0x00007eff03a22000)
$ LD_LIBRARY_PATH=`pwd`/lib myDemo
hello world!
$ mv lib/libexample.so ~/.local/lib64
$ LD_LIBRARY_PATH=`pwd`/lib myDemo
./myDemo: error while loading shared libraries...
To fix it, you must adjust the LD_LIBRARY_PATH to match the library's new location:
$ LD_LIBRARY_PATH=~/.local/lib64 myDemo
hello world!
• You're compiling software that needs to link against a library that itself has just been
compiled and has not yet been installed. Good build systems, such as Autotools and
CMake, can help handle this.
• You're bundling software that's designed to run out of a single directory, with no install
script or an install script that places libraries in non-standard directories. Several
applications have releases that a Linux user can download, copy to /opt, and run with
"no install." The LD_PATH_LIBRARY variable gets set through wrapper scripts so the
user often isn't even aware it's been set.
Compiling software gives you a lot of flexibility in how you run your system. The
LD_LIBRARY_PATH variable, along with the -L and -l GCC options, are components of that
flexibility.
By Stephan Avenwedde
Linux, in a way, is a series of static and dynamic libraries that depend on each other. For new
users of Linux-based systems, the whole handling of libraries can be a mystery. But with
experience, the massive amount of shared code built into the operating system can be an
advantage when writing new applications.
To help you get in touch with this topic, I prepared a small application example that shows the
most common methods that work on common Linux distributions (these have not been
tested on other systems). To follow along with this hands-on tutorial using the example
application, open a command prompt and type:
my_app
libmy_static.a
libmy_shared.so
A static library is an archive (ar) of object files. The object files are usually in the ELF format.
ELF is short for Executable and Linkable Format, which is compatible with many operating
systems.
The output of the file command tells you that the static library libmy_static.a is the ar
archive type:
$ file libmy_static.a
libmy_static.a: current ar archive
With ar -t, you can look into this archive; it shows two object files:
$ ar -t libmy_static.a
libmy_static_a.o
libmy_static_b.o
You can extract the archive's files with ar -x <archive-file>. The extracted files are
object files in ELF format:
$ ar -x libmy_static.a
$ file libmy_static_a.o
libmy_static_a.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not
stripped
Another thing to note is that when a bug is fixed in a shared library, every application that
references this library will profit from it. This also means that if the bug remains undetected,
each referencing application will suffer from it (if the application uses the affected parts).
It can be very hard for beginners when an application requires a specific version of the library,
but the linker only knows the location of an incompatible version. In this case, you must help
the linker find the path to the correct version.
Although this is not an everyday issue, understanding dynamic linking will surely help you in
fixing such problems. Fortunately, the mechanics for this are quite straightforward.
To detect which libraries are required for an application to start, you can use ldd, which will
print out the shared libraries used by a given file:
$ ldd my_app
linux-vdso.so.1 (0x00007ffd1299c000)
libmy_shared.so => not found
libc.so.6 => /lib64/libc.so.6 (0x00007f56b869b000)
/lib64/ld-linux-x86-64.so.2 (0x00007f56b8881000)
Note that the library libmy_shared.so is part of the repository but is not found. This is
because the dynamic linker, which is responsible for loading all dependencies into memory
before executing the application, cannot find this library in the standard locations it searches.
Errors associated with linkers finding incompatible versions of common libraries (like bzip2,
for example) can be quite confusing for a new user. One way around this is to add the
repository folder to the environment variable LD_LIBRARY_PATH to tell the linker where to
look for the correct version. In this case, the right version is in this folder, so you can export it:
$ LD_LIBRARY_PATH=$(pwd):$LD_LIBRARY_PATH
$ export LD_LIBRARY_PATH
$ ldd my_app
linux-vdso.so.1 (0x00007ffd385f7000)
libmy_shared.so => /home/stephan/library_sample/libmy_shared.so
(0x00007f3fad401000)
libc.so.6 => /lib64/libc.so.6 (0x00007f3fad21d000)
/lib64/ld-linux-x86-64.so.2 (0x00007f3fad408000)
$ file my_app
my_app: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2,
BuildID[sha1]=26c677b771122b4c99f0fd9ee001e6c743550fa6, for GNU/Linux 3.2.0, not
stripped
$ file /lib64/ld-linux-x86-64.so.2
/lib64/ld-linux-x86-64.so.2: symbolic link to ld-2.31.so
Looking back to the output of ldd, you can also see (next to libmy_shared.so) that each
dependency ends with a number (e.g., /lib64/libc.so.6). The usual naming scheme of
shared objects is:
On my system, libc.so.6 is also a symbolic link to the shared object libc-2.30.so in the
same folder:
$ file /lib64/libc.so.6
/lib64/libc.so.6: symbolic link to libc-2.31.so
If you are facing the issue that an application will not start because the loaded library has the
wrong version, it is very likely that you can fix this issue by inspecting and rearranging the
symbolic links or specifying the correct search path (see "The dynamic loader: ld.so" below).
ld.so looks for shared objects in these places in the following order:
1. The relative or absolute path in the application (hardcoded with the -rpath compiler
option on GCC)
2. In the environment variable LD_LIBRARY_PATH
3. In the file /etc/ld.so.cache
Keep in mind that adding a library to the systems library archive /usr/lib64 requires
administrator privileges. You could copy libmy_shared.so manually to the library archive
and make the application work without setting LD_LIBRARY_PATH:
unset LD_LIBRARY_PATH
sudo cp libmy_shared.so /usr/lib64/
When you run ldd, you can see the path to the library archive shows up now:
$ ldd my_app
linux-vdso.so.1 (0x00007ffe82fab000)
libmy_shared.so => /lib64/libmy_shared.so (0x00007f0a963e0000)
libc.so.6 => /lib64/libc.so.6 (0x00007f0a96216000)
/lib64/ld-linux-x86-64.so.2 (0x00007f0a96401000)
Modify the makefile (line 10) and recompile the program by invoking make -B . Then, the
output of ldd shows libmy_shared.so is listed with its absolute path.
CFLAGS =/home/stephan/library_sample/libmy_shared.so
Then recompile:
$ make
Confirm it is using the absolute path you set, which you can see on line 2 of the output:
$ ldd my_app
linux-vdso.so.1 (0x00007ffe143ed000)
libmy_shared.so => /lib64/libmy_shared.so (0x00007fe50926d000)
/home/stephan/library_sample/libmy_shared.so (0x00007fe509268000)
libc.so.6 => /lib64/libc.so.6 (0x00007fe50909e000)
/lib64/ld-linux-x86-64.so.2 (0x00007fe50928e000)
This is a good example, but how would this work if you were making a library for others to use?
New library locations can be registered by writing them to /etc/ld.so.conf or creating a
<library-name>.conf file containing the location under /etc/ld.so.conf.d/.
Afterward, ldconfig must be executed to rewrite the ld.so.cache file. This step is
sometimes necessary after you install a program that brings some special shared libraries with
it.
• 32 bit: /usr/lib
• 64 bit: /usr/lib64
Debian family
• 32 bit: /usr/lib/i386-linux-gnu
• 64 bit: /usr/lib/x86_64-linux-gnu
• 32 bit: /usr/lib32
• 64 bit: /usr/lib64
• 32bit: /usr/lib32
• 64bit: /usr/lib
Knowing where to look for these key libraries can make broken library links a problem of the
past.
By Jayashree Huttanagoudar
Code for applications written using C usually has multiple source files, but ultimately you will
need to compile them into a single executable.
You can do this in two ways: by creating a static library or a dynamic library (also called a
shared library). These two types of libraries vary in terms of how they are created and linked.
Your choice of which to use depends on your use case.
Static libraries are created by copying all necessary library modules used in a program into the
final executable image. The linker links static libraries as a last step in the compilation process.
An executable is created by resolving external references, combining the library routines with
program code.
Create add.c, sub.c , mult.c and divi.c with these function definitions:
// add.c
int add(int a, int b){
return (a+b);
}
//sub.c
int sub(int a, int b){
return (a-b);
}
//mult.c
int mult(int a, int b){
return (a*b);
}
//divi.c
int divi(int a, int b){
return (a/b);
}
Now generate object files add.o, sub.o, mult.o, and divi.o using GCC:
The -c option skips the linking step and creates only object files.
Create a static library called libmymath.a, then remove the object files, as they're no longer
required. (Note that using a trash command is safer than rm.)
Next, use your math library in some custom code and then link it.
#include <mymath.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
int x, y;
printf("Enter two numbers\n");
scanf("%d%d",&x,&y);
printf("\n%d + %d = %d", x, y, add(x, y));
printf("\n%d - %d = %d", x, y, sub(x, y));
printf("\n%d * %d = %d", x, y, mult(x, y));
if(y==0){
printf("\nDenominator is zero so can't perform division\n");
exit(0);
}else{
printf("\n%d / %d = %d\n", x, y, divi(x, y));
return 0;
}
}
Notice that the first line is an include statement referencing, by name, your own
libmymath library.
$ gcc -I . -c mathDemo.c
The -I option tells GCC to search for header files listed after it. In this case, you're
specifying the current directory, represented by a single dot (.).
Alternately, you can specify the library path along with the library name:
In the latter example, the -lmymath option tells the linker to link the object files present in
the libmymath.a with the object file mathDemo.o to create the final executable. The -L
option directs the linker to look for libraries in the following argument (similar to what you
would do with -I).
$ file mathDemo
mathDemo: ELF 64-bit LSB executable, x86-64...
statically linked, with debug_info, not stripped
Using the ldd command, you can see that the executable is not dynamically linked:
$ ldd ./mathDemo
not a dynamic executable
$ du -h ./mathDemo
932K ./mathDemo
By comparison, the dynamic executable of the same code took up just 24K.
$ ./mathDemo
Enter two numbers
10
5
10 + 5 = 15
10 - 5 = 5
10 * 5 = 50
10 / 5 = 2
Looks good!
When you use static linking, a library's code gets "hidden" within the executable you create,
meaning the only way to patch it is to re-compile and re-release a new executable every time
a library gets an update—and you have better things to do with your time, trust me.
However, static linking is a reasonable option if the code of a library exists either in the same
code base as the executable using it or in specialized embedded devices that are expected to
receive no updates.
By Seth Kenlon
If you're a developer creating binary packages, like an RPM, DEB, Flatpak, or Snap, you have
to compile code for a variety of different target platforms. Typical targets include 32-bit and
64-bit x86 and ARM. You could do your builds on different physical or virtual machines, but
that means maintaining several systems. Instead, you can use the GNU Compiler Collection
(GCC) to cross-compile, producing binaries for several different architectures from a single
build machine.
Assume you have a simple dice-rolling game that you want to cross-compile. Something
written in C is relatively easy on most systems, so to add complexity for the sake of realism, I
wrote this example in C++, so the program depends on something not present in C
(iostream, specifically).
#include <iostream>
#include <cstdlib>
int main() {
int i;
do {
cout << "Pick a number between 1 and 20: \n";
cin >> i;
int c = rand ( ) % 21;
if (i > 20) lose (c);
else if (i < c ) lose (c);
else if (i > c ) win (c);
void draw ( )
{
cout << "What are the chances. You tied. Try again, I dare you! \n";
}
$ ./dice
Pick a number between 1 and 20:
[...]
You can see what kind of binary you just produced with the file command:
$ file ./dice
dice: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically
linked (uses shared libs), for GNU/Linux 5.1.15, not stripped
$ ldd dice
linux-vdso.so.1 => (0x00007ffe0d1dc000)
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6
(0x00007fce8410e000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
(0x00007fce83d4f000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6
(0x00007fce83a52000)
You have confirmed two things from these tests: The binary you just ran is 64-bit, and it is
linked to 64-bit libraries.
That means that, in order to cross-compile for 32-bit, you must tell g++ to:
Regardless of the system you're using, you also must install any 32-bit libraries your project
uses. For instance, if you include yaml-cpp in your project, then you must install the 32-bit
version of yaml-cpp or, on many systems, the development package for yaml-cpp (for
instance, yaml-cpp-devel on Fedora) before compiling it.
The -m32 flag tells GCC to compile in 32-bit mode. The -march=i686 option further defines
what kind of optimizations to use (refer to info gcc for a list of options). The -L flag sets
the path to the libraries you want GCC to link to. This is usually /usr/lib for 32-bit,
although, depending on how your system is set up, it could be /usr/lib32 or even
/opt/usr/lib or any place you know you keep your 32-bit libraries.
$ file ./dice32
dice: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV),
dynamically linked (uses shared libs) [...]
Different architectures
Compiling 32-bit on 64-bit for the same processor family allows GCC to make many
assumptions about how to compile the code. If you need to compile for an entirely different
processor, you must install the appropriate cross-build GCC utilities. Which utility you install
depends on what you are compiling. This process is a little more complex than compiling for
the same CPU family.
When you're cross-compiling for the same family, you can expect to find the same set of 32-
bit libraries as 64-bit libraries, because your Linux distribution is maintaining both. When
compiling for an entirely different architecture, you may have to hunt down libraries required
by your code. The versions you need may not be in your distribution's repositories because
your distribution may not provide packages for your target system, or it may not mirror all
packages in a convenient location. If the code you're compiling is yours, then you probably
have a good idea of what its dependencies are and possibly where to find them. If the code is
something you have downloaded and need to compile, then you probably aren't as familiar
with its requirements. In that case, investigate what the code requires to build correctly
(they're usually listed in the README or INSTALL files, and certainly in the source code itself),
then go gather the components.
For example, if you need to compile C code for ARM, you must first install gcc-arm-linux-gnu
(32-bit) or gcc-aarch64-linux-gnu (64-bit) on Fedora or RHEL, or arm-linux-gnueabi-gcc
and binutils-arm-linux-gnueabi on Ubuntu. This provides the commands and libraries you
need to build (at least) a simple C program. Additionally, you need whatever libraries your
code uses. You can place header files in the usual location (/usr/include on most
systems), or you can place them in a directory of your choice and point GCC to it with the -I
option.
$ arm-linux-gnu-g++ dice.cpp \
-I/home/seth/src/crossbuild/arm/cpp \
-o armdice.bin
$ file armdice.bin
armdice.bin: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV) [...]