This post is a guide to build a GCC toolchain. The GCC toolchain is portable and self-contained
(the only dependency is the Linux kernel).
I use docker to set the environment up.
TL;DR: Just download the all-in-one script in the release page
and use it.
Prerequisite
Preparation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| # Use ubuntu:22.04 image
docker run -it ubuntu:22.04 bash
cd /root
apt update && apt upgrade --yes
# Install dependencies
DEBIAN_FRONTEND=noninteractive apt install --yes \
build-essential texinfo bison git curl rsync gawk \
python-is-python3 help2man file
# Download sources
mkdir packages
curl -L 'https://ftpmirror.gnu.org/binutils/binutils-2.42.tar.xz' \
-o packages/binutils-2.42.tar.xz
curl -L 'https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.30.tar.xz' \
-o packages/linux-6.6.30.tar.xz
curl -L 'https://ftpmirror.gnu.org/glibc/glibc-2.39.tar.xz' \
-o packages/glibc-2.39.tar.xz
curl -L 'https://ftpmirror.gnu.org/gcc/gcc-14.1.0/gcc-14.1.0.tar.xz' \
-o packages/gcc-14.1.0.tar.xz
curl -L 'https://github.com/besser82/libxcrypt/releases/download/v4.4.36/libxcrypt-4.4.36.tar.xz' \
-o packages/libxcrypt-4.4.36.tar.xz
|
The layout in the disk:
1
2
3
4
5
6
| packages/
|-- binutils-2.42.tar.xz
|-- gcc-14.1.0.tar.xz
|-- glibc-2.39.tar.xz
|-- libxcrypt-4.4.36.tar.xz
`-- linux-6.6.30.tar.xz
|
Extract all sources.
1
| find packages/ -mindepth 1 -exec tar -Jxf {} \;
|
1
2
3
4
5
6
7
8
| # Layout
.
|-- binutils-2.42
|-- gcc-14.1.0
|-- glibc-2.39
|-- libxcrypt-4.4.36
|-- linux-6.6.30
`-- packages
|
Overview
Build and install cross GCC toolchain (NOTE: the corss here is not accurate, because the target architecture is
the same as the build one).
- Build and install cross Binutils.
- Install Linux Kernel headers.
- Build stage1 cross GCC
- Build stage1 glibc
- Build and install headers
stubs.h
and libraries crt1.o
crti.o
crtn.o
libc.so
…
- Build stage2 cross GCC
- Build and install final glibc
- Build and install final cross GCC
Build final GCC toolchain by the cross GCC toolchain.
Build and install libxcrypt by the final GCC toolchain.
Environment
1
2
3
4
5
6
| export PREFIX='/opt/toolchain'
export TARGET='x86_64-linux-gnu'
export CROSS_PREFIX="${PREFIX}/cross"
export TARGET_PREFIX="${CROSS_PREFIX}/${TARGET}"
export ARCH='x86'
export PATH="/opt/toolchain/bin:/opt/toolchain/cross/bin:${PATH}"
|
NOTE: The path /opt/toolchain/cross
is temporary and will be removed after setting all up. The final path is
/opt/toolchain
.
1
2
3
4
5
| mkdir -p "${CROSS_PREFIX}"
ln -snf .. "${CROSS_PREFIX}/${TARGET}"
mkdir -p "${TARGET_PREFIX}/lib"
ln -snf "lib" "${TARGET_PREFIX}/lib64"
|
1
2
3
4
5
6
7
| # Layout
/opt/
`-- toolchain
|-- cross
| `-- x86_64-linux-gnu -> ..
|-- lib
`-- lib64 -> lib
|
- Build and install cross Binutils.
1
2
3
4
5
6
7
8
9
| pushd binutils-2.42
./configure --prefix="${CROSS_PREFIX}" \
--target="${TARGET}" \
--disable-multilib
make -j "$(nproc)"
make install
popd
|
- We specified
x86_64-linux-gnu
as the target system type. --disable-multilib
means that we only want our Binutils installation to work with programs and libraries using the
64-bits instruction set.
- Install Linux Kernel headers.
1
2
3
4
5
| pushd linux-6.6.30
make ARCH="${ARCH}" INSTALL_HDR_PATH="${TARGET_PREFIX}" headers_install
popd
|
- We installed the headers to
/opt/toolchain/include
.
- Build stage1 cross GCC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| pushd gcc-14.1.0
# Download prerequisites
contrib/download_prerequisites
mkdir build
cd build
../configure --prefix="${CROSS_PREFIX}" \
--target="${TARGET}" \
--libdir="${TARGET_PREFIX}/lib" \
--libexecdir="${TARGET_PREFIX}/libexec" \
--with-local-prefix="${TARGET_PREFIX}" \
--with-gxx-include-dir="${TARGET_PREFIX}/include/c++" \
--enable-languages=c,c++ \
--enable-default-pie \
--disable-multilib \
--disable-libssp \
--disable-threads \
--disable-libatomic \
--disable-libffi \
--disable-libgomp \
--disable-libitm \
--disable-libquadmath \
--disable-libquadmath-support \
--disable-libsanitizer \
--disable-fixincludes \
--disable-bootstrap
make -j "$(nproc)" all-gcc
make install-gcc
popd
|
- We specified
x86_64-linux-gnu
as the target system type. - We installed the executables to
/opt/toolchain/cross/bin
and installed headers and libraries to /opt/toolchain/include
and /opt/toolchain/lib
respectively. - We enabled
C
and C++
and disabled many libraries to accelerate the build time.
- Build stage1 glibc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| pushd glibc-2.39
mkdir build
cd build
../configure --prefix="${TARGET_PREFIX}" \
--build="${MACHTYPE}" \
--host="${TARGET}" \
--target="${TARGET}" \
--with-headers="${TARGET_PREFIX}"/include \
--disable-multilib \
libc_cv_forced_unwind=yes
make install-bootstrap-headers=yes install-headers
make -j "$(nproc)" csu/subdir_lib
install csu/crt1.o csu/crti.o csu/crtn.o "${TARGET_PREFIX}"/lib
"${TARGET}-gcc" -nostdlib -nostartfiles -shared -x c /dev/null -o "${TARGET_PREFIX}"/lib/libc.so
touch "${TARGET_PREFIX}"/include/gnu/stubs.h
popd
|
- We installed the libraries to
/opt/toolchain/lib
. - We installed the C library’s startup files,
crt1.o
, crti.o
and crtn.o
to the installation directory manually.
There’s doesn’t seem to a make rule that does this without having other side effects. - We created a couple of dummy files,
libc.so
and stubs.h
, which are expected in step #5
, but which will be
replaced by step #6
.
- Build stage2 cross GCC
1
2
3
4
5
6
7
8
| pushd gcc-14.1.0
cd build
make -j "$(nproc)" all-target-libgcc
make install-target-libgcc
popd
|
- Build and install final glibc
1
2
3
4
5
6
7
8
| pushd glibc-2.39
cd build
make -j "$(nproc)"
make install
popd
|
- Build and install final cross GCC
1
2
3
4
5
6
7
8
| pushd gcc-14.1.0
cd build
make -j "$(nproc)"
make install
popd
|
Validation
Now, we have a corss GCC toolchain, we can test the toolchain whether it works as expect.
1
2
3
4
5
6
| #include <iostream>
auto main() -> int {
std::cout << "Hello, world!" << std::endl;
return 0;
}
|
1
2
3
4
5
| $ x86_64-linux-gnu-g++ -Wl,-rpath,/opt/toolchain/lib \
-Wl,-dynamic-linker,/opt/toolchain/lib/ld-linux-x86-64.so.2 \
hello.cc -o hello
$ ./hello
Hello, world!
|
It works!
- Build and install final Binutils.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| rm -rf binutils-2.42
tar -Jxf packages/binutils-2.42.tar.xz
pushd binutils-2.42
LDFLAGS="-L${PREFIX}/lib -Wl,-rpath,${PREFIX}/lib \
-Wl,-dynamic-linker,$(find "${PREFIX}/lib" -name 'ld-linux-*')" \
./configure --prefix="${PREFIX}" \
--host="${TARGET}" \
--enable-gold \
--enable-plugins \
--disable-multilib
make -j "$(nproc)"
make install-strip
# Remove the old files
rm -rf "${PREFIX}/lib/ldscripts"
popd
|
- Build and install final GCC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| rm -rf gcc-14.1.0
tar -Jxf packages/gcc-14.1.0.tar.xz
# Download prerequisites
pushd gcc-14.1.0
contrib/download_prerequisites
mkdir build
cd build
ldflags="-L${PREFIX}/lib -Wl,-rpath,${PREFIX}/lib \
-Wl,-dynamic-linker,$(find "${PREFIX}/lib" -name 'ld-linux-*')"
LDFLAGS="${ldflags}" LDFLAGS_FOR_TARGET="${ldflags}" \
../configure --prefix="${PREFIX}" \
--host="${TARGET}" \
--with-local-prefix="${PREFIX}" \
--enable-languages=c,c++ \
--enable-default-pie \
--disable-multilib \
--disable-fixincludes
make BOOT_LDFLAGS="${ldflags}" -j "$(nproc)"
# Remove the old files
rm -rf "${PREFIX}/include/c++"
make install-strip
popd
|
- Build and install final glibc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| rm -rf glibc-2.39
tar -Jxf packages/glibc-2.39.tar.xz
pushd glibc-2.39
mkdir build
cd build
../configure --prefix="${PREFIX}" \
--with-headers="${PREFIX}"/include \
--disable-multilib
make -j "$(nproc)"
make install
popd
|
- Build and install libxcrypt
1
2
3
4
5
6
7
8
9
| pushd libxcrypt-4.4.36
./configure --prefix="${PREFIX}"
make -j "$(nproc)"
make install
rm -rf "${PREFIX}/lib/pkgconfig"
popd
|
We can use the GCC toolchain like this.
1
2
3
4
5
| $ g++ -Wl,-rpath,/opt/toolchain/lib \
-Wl,-dynamic-linker,/opt/toolchain/lib/ld-linux-x86-64.so.2 \
hello.cc -o hello
$ ./hello
Hello, world!
|
However, it is verbose. We can use GCC specs file to simplify the usage.
1
2
3
4
5
6
7
8
9
| toolchain_home='/opt/toolchain'
interpreter="$(find "${toolchain_home}" -name "ld-linux-*")"
filename="$(basename "${interpreter}")"
dirname="$(dirname "${interpreter}")"
"${toolchain_home}/bin/gcc" -dumpspecs | sed "{
s|:\([^;}:]*\)/\(${filename/.so*/}\)|:${dirname}/\2|g
s|collect2|collect2 -rpath ${dirname}|
}" >"${toolchain_home}/lib/gcc/$(uname -m)-linux-gnu/specs"
|
Validation
1
2
3
4
5
6
7
8
9
10
| $ gcc -v
Using built-in specs.
Reading specs from /opt/toolchain/lib/gcc/x86_64-linux-gnu/specs
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/opt/toolchain/libexec/gcc/x86_64-linux-gnu/14.1.0/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ../configure --prefix=/opt/toolchain --host=x86_64-linux-gnu --with-local-prefix=/opt/toolchain --enable-languages=c,c++ --enable-default-pie --disable-multilib
Thread model: posix
Supported LTO compression algorithms: zlib
gcc version 14.1.0 (GCC)
|
Now, GCC
uses the specs file /opt/toolchain/lib/gcc/x86_64-linux-gnu/specs
which was created by us.
1
2
3
| $ g++ hello.cc -o hello
$ ./hello
Hello, world!
|
It works!
So far, we have built our GCC toolchain. In this section, we use patchelf
to make the toolchain portable.
1
2
3
4
5
6
| mkdir -p /opt/patchelf
curl -L "https://github.com/NixOS/patchelf/releases/download/0.16.1/patchelf-0.16.1-$(uname -m).tar.gz" \
-o - | tar -zxv -C /opt/patchelf
export PATH="/opt/patchelf/bin:${PATH}"
|
We move the GCC toolchain to a different location to pretend that we extracted the toolchain to a new machine.
1
| mv /opt/toolchain /root/
|
We will find the executable GCC
can’t be executed.
1
2
3
| $ cd /root/toolchain/bin
$ ./gcc --version
bash: ./gcc: No such file or directory
|
We can use the tool patchelf
to fix this issue.
1
2
3
4
5
6
7
| $ patchelf --set-rpath /root/toolchain/lib gcc
$ patchelf --set-interpreter /root/toolchain/lib/ld-linux-x86-64.so.2 gcc
$ ./gcc --version
gcc (GCC) 14.1.0
Copyright (C) 2024 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
For the sake of convenience, we can use the following function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| function relocate() {
local opts
local overwrite=false
if ! opts="$(getopt -o '' -l 'overwrite' -- "${@}")"; then
return 1
fi
eval "set -- ${opts}"
while true; do
case "${1}" in
--overwrite)
overwrite=true
shift
;;
--)
shift
break
;;
*)
echo 'Invalid arguments'
return 1
;;
esac
done
local search_path="${1}"
local base_path="${2}"
local libc_so
local library_path
local interpreter
libc_so="$(find "${search_path}" -name 'libc.so.6')"
library_path="$(dirname "${libc_so}")"
interpreter="$(find "${search_path}" -name 'ld-linux-*.so.*')"
local old_rpath
local new_rpath
while read -r file; do
if old_rpath="$(patchelf --print-rpath "${file}" 2>/dev/null)"; then
if ${overwrite}; then
old_rpath="\$ORIGIN:\$ORIGIN/lib64:\$ORIGIN/lib:\$ORIGIN/../lib64:\$ORIGIN/../lib"
fi
new_rpath="${library_path}${old_rpath:+:${old_rpath}}"
patchelf --set-rpath "${new_rpath}" "${file}"
if readelf -S "${file}" | grep '.interp' >/dev/null; then
patchelf --set-interpreter "${interpreter}" "${file}"
fi
fi
done < <(find "$(readlink -f "${base_path}")" -type f)
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| $ ./g++ --version
bash: ./g++: No such file or directory
$ relocate --overwrite /root/toolchain g++
$ patchelf --print-rpath g++
/root/toolchain/lib:$ORIGIN:$ORIGIN/lib64:$ORIGIN/lib:$ORIGIN/../lib64:$ORIGIN/../lib
$ patchelf --print-interpreter g++
/root/toolchain/lib/ld-linux-x86-64.so.2
$ ./g++ --version
g++ (GCC) 14.1.0
Copyright (C) 2024 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
Next, we fix the whole GCC toolchain by apply the following function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
| function configure_toolchain() {
local PATCHELF='patchelf'
local LIBC_SO='libc.so.6'
local INTERPRETER='ld-linux-*'
local READELF='readelf'
local prefix="${1}"
echo -e 'Configuring the toolchain...'
if ! cd "${prefix}"; then
echo -e "Failed to change the current directory to \033[34;1m${prefix}/${TOOLCHAIN_DIRNAME}\033[0m ."
fi
local patchelf
patchelf="$(command -v "${PATCHELF}")"
if [[ ! -f "${patchelf}" ]]; then
echo -e "Failed to find the tool \033[34;1m${patchelf}\033[0m ."
fi
echo -e "Program ${PATCHELF} was found: \033[34;1m${patchelf}\033[0m"
local readelf="/usr/bin/readelf"
if [[ ! -f "${readelf}" ]]; then
echo -e "Failed to find the tool \033[34;1m${readelf}\033[0m ."
fi
echo -e "Program ${READELF} was found: \033[34;1m${readelf}\033[0m"
local interpreter
if ! interpreter="$(find "$(pwd)" -name "${INTERPRETER}")" || [[ -z "${interpreter}" ]]; then
echo -e "Failed to find the interpreter \033[34;1m${INTERPRETER}\033[0m"
fi
echo -e "Interpreter was found: \033[34;1m${interpreter}\033[0m"
local libc_so
if ! libc_so="$(find "$(pwd)" -name "${LIBC_SO}")" || [[ -z "${libc_so}" ]]; then
echo -e "Failed to find the library \033[34;1m${LIBC_SO}\033[0m"
fi
echo -e "Library was found: \033[34;1m${libc_so}\033[0m"
# Remove useless files
rm -rf "$(pwd)/$(uname -m)-linux-gnu"/*
find "$(pwd)" -name '*.la' -delete
mkdir "$(pwd)/$(uname -m)-linux-gnu/include"
mkdir "$(pwd)/$(uname -m)-linux-gnu/lib"
ln -snf lib "$(pwd)/$(uname -m)-linux-gnu/lib64"
# Link headers
local include_path
include_path="$(pwd)/include"
pushd "$(uname -m)-linux-gnu/include" >/dev/null || exit
while read -r path; do
local absolute_path="${include_path}/${path}"
if [[ -d "${absolute_path}" ]]; then
mkdir -p "${path}"
else
ln -snf "${absolute_path}" "${path}"
fi
done < <(find "${include_path}" -mindepth 1 -printf '%P\n')
popd >/dev/null || exit
local rpaths=(
"$(pwd)/$(uname -m)-linux-gnu/lib"
"$(dirname "${libc_so}")"
"\$ORIGIN"
"\$ORIGIN/lib64"
"\$ORIGIN/lib"
"\$ORIGIN/../lib64"
"\$ORIGIN/../lib"
)
local rpaths_in_line
rpaths_in_line="$(
IFS=':'
echo "${rpaths[*]}"
)"
while read -r file; do
if ! file "${file}" | grep ELF >/dev/null; then
continue
fi
"${patchelf}" --set-rpath "${rpaths_in_line}" "${file}"
if "${readelf}" -S "${file}" | grep '.interp' >/dev/null; then
"${patchelf}" --set-interpreter "${interpreter}" "${file}"
fi
done < <(
find "$(pwd)" -type f ! -name '*.o' ! -name '*.a' \
! -name "${PATCHELF}" ! -name "${INTERPRETER}" ! -name "${LIBC_SO}"
)
"${patchelf}" --set-interpreter "${interpreter}" "${libc_so}"
pushd bin >/dev/null || exit
ln -snf gcc cc
popd >/dev/null || exit
# Modify files
local current_path
current_path="$(pwd)"
while read -r file; do
sed -i "s|/opt/toolchain|${current_path}|g" "${file}"
done < <(
find . -type f -exec grep -E -I -l $'[=\'" ]/opt/toolchain' {} \;
)
# Modify ldd
sed -i "s|RTLDLIST=.*|RTLDLIST='${interpreter}'|" bin/ldd
# Configure gcc specs
local filename
filename="$(basename "${interpreter}")"
local dirname
dirname="$(dirname "${interpreter}")"
"$(pwd)/bin/gcc" -dumpspecs | sed "{
s|:\([^;}:]*\)/\(${filename/.so*/}\)|:${dirname}/\2|g
s|collect2|collect2 -rpath ${rpaths_in_line}|
}" >"$(pwd)/lib/gcc/specs"
}
|
1
| $ configure_toolchain /root/toolchain
|
Validation
1
2
3
4
5
| $ export PATH="/root/toolchain/bin:${PATH}"
$ cd /root/
$ g++ hello.cc -o hello
$ ./hello
Hello, world!
|
It works!
Use the all-in-one script
I made an all-in-one script to install a portable GCC toolchain. See the release page.
Example
1
2
3
4
5
| curl -LO 'https://github.com/adonis0147/devel-env/releases/download/v2024.05/install_toolchain_x86_64.sh'
bash install_toolchain_x86_64.sh /root
export PATH="/root/compiler/bin:${PATH}"
|
References
- How to Build a GCC Cross-Compiler
Author
Adonis Ling
LastMod
2024-10-30
(ee89427)