検証環境
CentOS 8.1 を利用する。
]# cat /etc/redhat-release
CentOS Linux release 8.1.1911 (Core)
]# uname -a
Linux localhost.localdomain 4.18.0-147.3.1.el8_1.x86_64 #1 SMP Fri Jan 3 23:55:26 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
/proc/kallsyms ってなに?
アドレス・シンボルの種類・シンボル名が表示できるファイル。
]# cat /proc/kallsyms | head
// 仮想アドレス 種類 シンボル名
0000000000000000 A irq_stack_union
0000000000000000 A __per_cpu_start
0000000000004000 A cpu_debug_store
0000000000005000 A cpu_tss_rw
0000000000008000 A gdt_page
0000000000009000 A exception_stacks
000000000000e000 A entry_stack_storage
000000000000f000 A espfix_waddr
000000000000f008 A espfix_stack
000000000000f010 A cpu_llc_id
シンボルの種類(A
とか)が示す意味は nm と同じ(というか nm で作られてる値)。各値の詳細は nm のマニュアルに記載されている。
"A" The symbol's value is absolute, and will not be changed by further linking.
"B"
"b" The symbol is in the uninitialized data section (known as BSS).
"C" The symbol is common. Common symbols are uninitialized data. When linking, multiple common symbols may appear with the same name. If
the symbol is defined anywhere, the common symbols are treated as undefined references.
...
引用元: nm マニュアル
なお kptr_restrict が 2 だと /proc/kallsyms は出力されない。
- 0: 常に出力する
- 1: は CAP_SYSLOG 権限を持ってる場合だけ出力する
- 2: 常に出力しない
]# cat /proc/sys/kernel/kptr_restrict
0
誰が利用するの?
主に SystemTap や perf といったコマンドが、アドレスとシンボル名を解決するために利用する。/proc/kallsyms
を直接参照する場合もあれば、これと同じ情報をカーネル内の関数から参照する場合もある。後者が多いと思う。
また、KASLR が有効かを確認するときにも利用する。System.map と出力を比較し、同一なら KASLR は無効。異なるなら KASLR は有効。
// kaslr が有効
]# cat /boot/System.map-$(uname -r) | grep ksys_write
ffffffff812b9d10 T ksys_write
]# cat /proc/kallsyms| grep ksys_write
ffffffff9aeb9d10 T ksys_write
なお System.map はアドレスとシンボルが格納されているファイル。System.map はビルド時のアドレスで /proc/kallsyms は実行時のアドレスという違いがある。最近のカーネルは脆弱性対策のためにカーネルの領域をランダムにずらして配置する(KASLR)。そのため、 KASLR が有効なら System.map とは一致せず、 KASLR が無効なら System.map と一致する。KASLR についての詳細はASLRとKASLRの概要を参照してください。
どうやって出力してるの?
コードを読んで調べた結果をまとめる。
/proc/kallsyms
は kernel/kallsyms.c でシンボル情報を加工して出力する。- シンボル情報は、カーネルビルド時にカーネルに対する nm の結果をもとに作成する。
以下、細かい話なので、興味がある人は読んでください。
コードリーディング
まず、vmlinux 自体にシンボル情報が付与されているかを調べる。最近はカーネルソースコードの scripts/extract-vmlinux で vmlinuz を展開できるのでこれを使う。カーネルコードが手元になければ、昔ながらの方法のほうが手軽な気がする。(参考 vmlinuz から vmlinux を抽出する方法)
]# ./extract-vmlinux /boot/vmlinuz-$(uname -r ) > vmlinux
カーネルはシンボルが strip されるので、シンボルテーブルを表示しても空。また、 file コマンドで表示しても stripped されていることがわかる。
]# readelf -s vmlinux
]# file vmlinux
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=7c2f12f6c14353ad557dee67e04ee11fb7e9b2ba, stripped
ちなみに strip するのはビルドスクリプト内っぽい。
cp_vmlinux()
{
eu-strip --remove-comment -o "$2" "$1"
}
...
BuildKernel() {
...
# When the bootable image is just the ELF kernel, strip it.
# We already copy the unstripped file into the debuginfo package.
if [ "$KernelImage" = vmlinux ]; then
CopyKernel=cp_vmlinux
else
CopyKernel=cp
fi
...
$CopyKernel $KernelImage \
$RPM_BUILD_ROOT/%{image_install_path}/$InstallName-$KernelVer
ということで、シンボル情報はどっか別の場所に持っているはず。まずは出力部分を調べると、 kernel/kallsyms.c の s_show で、kallsym_iter を単に出力しているっぽい。( procfs 自体については linux procfs 徹底入門 を参照してください)
kernel/kallsyms.c
static const struct seq_operations kallsyms_op = {
.start = s_start,
.next = s_next,
.stop = s_stop,
.show = s_show
};
static int s_show(struct seq_file *m, void *p)
{
void *value;
struct kallsym_iter *iter = m->private;
/* Some debugging symbols have no name. Ignore them. */
if (!iter->name[0])
return 0;
value = iter->show_value ? (void *)iter->value : NULL;
if (iter->module_name[0]) {
char type;
/*
* Label it "global" if it is exported,
* "local" if not exported.
*/
type = iter->exported ? toupper(iter->type) :
tolower(iter->type);
seq_printf(m, "%px %c %s\t[%s]\n", value,
type, iter->name, iter->module_name);
} else
seq_printf(m, "%px %c %s\n", value,
iter->type, iter->name);
return 0;
}
kallsym_iter は次の部分で設定されている。ひとまずメインっぽい get_ksymbol_core をみていく。
/* Returns false if pos at or past end of file. */
static int update_iter(struct kallsym_iter *iter, loff_t pos)
{
/* Module symbols can be accessed randomly. */
if (pos >= kallsyms_num_syms)
return update_iter_mod(iter, pos);
/* If we're not on the desired position, reset to new position. */
if (pos != iter->pos)
reset_iter(iter, pos);
iter->nameoff += get_ksymbol_core(iter);
iter->pos++;
return 1;
}
アドレスと種類とシンボル名を設定している。だんだんと核心に迫ってきました。
/* Returns space to next name. */
static unsigned long get_ksymbol_core(struct kallsym_iter *iter)
{
unsigned off = iter->nameoff;
iter->module_name[0] = '\0';
iter->value = kallsyms_sym_address(iter->pos);
iter->type = kallsyms_get_symbol_type(off);
off = kallsyms_expand_symbol(off, iter->name, ARRAY_SIZE(iter->name));
return off - iter->nameoff;
}
まずはアドレスから見ていくと、カーネルコンフィグごとに返すアドレスが違うっぽい。
static unsigned long kallsyms_sym_address(int idx)
{
if (!IS_ENABLED(CONFIG_KALLSYMS_BASE_RELATIVE))
return kallsyms_addresses[idx];
/* values are unsigned offsets if --absolute-percpu is not in effect */
if (!IS_ENABLED(CONFIG_KALLSYMS_ABSOLUTE_PERCPU))
return kallsyms_relative_base + (u32)kallsyms_offsets[idx];
/* ...otherwise, positive offsets are absolute values */
if (kallsyms_offsets[idx] >= 0)
return kallsyms_offsets[idx];
/* ...and negative offsets are relative to kallsyms_relative_base - 1 */
return kallsyms_relative_base - 1 - kallsyms_offsets[idx];
}
各コンフィグの意味は init/Kconfig に記述されている。KALLSYMS_BASE_RELATIVE はワードサイズを固定にするかどうか?まあよくわからんけど、ほぼ関係なさそう。
init/Kconfig
config KALLSYMS_ABSOLUTE_PERCPU
bool
depends on KALLSYMS
default X86_64 && SMP
config KALLSYMS_BASE_RELATIVE
bool
depends on KALLSYMS
default !IA64
help
Instead of emitting them as absolute values in the native word size,
emit the symbol references in the kallsyms table as 32-bit entries,
each containing a relative value in the range [base, base + U32_MAX]
or, when KALLSYMS_ABSOLUTE_PERCPU is in effect, each containing either
an absolute value in the range [0, S32_MAX] or a relative value in the
range [base, base + S32_MAX], where base is the lowest relative symbol
address encountered in the image.
On 64-bit builds, this reduces the size of the address table by 50%,
but more importantly, it results in entries whose values are build
time constants, and no relocation pass is required at runtime to fix
up the entries based on the runtime load address of the kernel.
今回使っているカーネルのコンフィグは次のようになっている。
]# cat /boot/config-$(uname -r) | grep CONFIG_KALLSYMS
CONFIG_KALLSYMS=y
CONFIG_KALLSYMS_ALL=y
CONFIG_KALLSYMS_ABSOLUTE_PERCPU=y
CONFIG_KALLSYMS_BASE_RELATIVE=y
ということで kallsyms_offsets[idx]
か kallsyms_relative_base - 1 - kallsyms_offsets[idx]
が返されるっぽい。
肝心の kallsyms_offsets
の定義は、次のように __weak シンボルになっている。つまり、このコード以外の場所で上書きされるはず。
kernel/kallsyms.c
/*
* These will be re-linked against their real values
* during the second link stage.
*/
extern const unsigned long kallsyms_addresses[] __weak;
extern const int kallsyms_offsets[] __weak;
extern const u8 kallsyms_names[] __weak;
...
ここ以外の場所で kallsyms_offsetes を探すと、次のコードが見つかった。グローバル変数を定義するアセンブリを出力してる。
scripts/kallsyms.c
...
output_label("kallsyms_offsets");
...
} else if (!symbol_absolute(&table[i])) {
if (_text <= table[i].addr)
printf("\tPTR\t_text + %#llx\n",
table[i].addr - _text);
else
fprintf(stderr, "kallsyms failure: "
"%s symbol value %#llx out of range in relative mode\n",
symbol_absolute(&table[i]) ? "absolute" : "relative",
table[i].addr);
exit(EXIT_FAILURE);
}
printf("\t.long\t%#x\n", (int)offset);
} else if (!symbol_absolute(&table[i])) {
if (_text <= table[i].addr)
printf("\tPTR\t_text + %#llx\n",
table[i].addr - _text);
else
printf("\tPTR\t_text - %#llx\n",
_text - table[i].addr);
} else {
printf("\tPTR\t%#llx\n", table[i].addr);
}
...
この scripts/kallsyms はカーネルビルド時(scripts/link-vmlinux.sh)に実行されている。指定されたオブジェクトファイルに nm を実行し、その結果をもとに scripts/kallsyms を実行してアセンブリを作成する。最後にアセンブリをコンパイルしてオブジェクトファイルを作成している。
scripts/link-vmlinux.sh
# Create ${2} .o file with all symbols from the ${1} object file
kallsyms()
{
info KSYM ${2}
local kallsymopt;
if [ -n "${CONFIG_KALLSYMS_ALL}" ]; then
kallsymopt="${kallsymopt} --all-symbols"
fi
if [ -n "${CONFIG_KALLSYMS_ABSOLUTE_PERCPU}" ]; then
kallsymopt="${kallsymopt} --absolute-percpu"
fi
if [ -n "${CONFIG_KALLSYMS_BASE_RELATIVE}" ]; then
kallsymopt="${kallsymopt} --base-relative"
fi
local aflags="${KBUILD_AFLAGS} ${KBUILD_AFLAGS_KERNEL} \
${NOSTDINC_FLAGS} ${LINUXINCLUDE} ${KBUILD_CPPFLAGS}"
local afile="`basename ${2} .o`.S"
${NM} -n ${1} | scripts/kallsyms ${kallsymopt} > ${afile}
${CC} ${aflags} -c -o ${2} ${afile}
}
kallsyms() の呼出は 4 段階に分かれている。いまいちわからんけど、最終的には先ほど作成したオブジェクトファイルを vmlinux_link でリンクしている。詳細を知りたい方は kallsyms(8) の man や scripts/link-vmlinux.sh
の先頭あたりのコメントを参照してください。
ということで、このコードで __weak シンボルが上書きされる。
scripts/link-vmlinux.sh
if [ -n "${CONFIG_KALLSYMS}" ]; then
# kallsyms support
# Generate section listing all symbols and add it into vmlinux
# It's a three step process:
# 1) Link .tmp_vmlinux1 so it has all symbols and sections,
# but __kallsyms is empty.
# Running kallsyms on that gives us .tmp_kallsyms1.o with
# the right size
# 2) Link .tmp_vmlinux2 so it now has a __kallsyms section of
# the right size, but due to the added section, some
# addresses have shifted.
# From here, we generate a correct .tmp_kallsyms2.o
# 3) That link may have expanded the kernel image enough that
# more linker branch stubs / trampolines had to be added, which
# introduces new names, which further expands kallsyms. Do another
# pass if that is the case. In theory it's possible this results
# in even more stubs, but unlikely.
# KALLSYMS_EXTRA_PASS=1 may also used to debug or work around
# other bugs.
# 4) The correct ${kallsymso} is linked into the final vmlinux.
#
# a) Verify that the System.map from vmlinux matches the map from
# ${kallsymso}.
kallsymso=.tmp_kallsyms2.o
kallsyms_vmlinux=.tmp_vmlinux2
# step 1
vmlinux_link "" .tmp_vmlinux1
kallsyms .tmp_vmlinux1 .tmp_kallsyms1.o
# step 2
...
info LD vmlinux
vmlinux_link "${kallsymso}" vmlinux
なお、 vmlinux_link は次のような処理になっていて、おもに built-in.a というアーカイブに引数で指定されたオブジェクトを追加していくような感じ?
scripts/link-vmlinux.sh
vmlinux_link()
{
local lds="${objtree}/${KBUILD_LDS}"
local objects
if [ "${SRCARCH}" != "um" ]; then
objects="--whole-archive \
built-in.a \
--no-whole-archive \
--start-group \
${KBUILD_VMLINUX_LIBS} \
--end-group \
${1}"
${LD} ${LDFLAGS} ${LDFLAGS_vmlinux} -o ${2} \
-T ${lds} ${objects}
else
肝心の build-in.a は archive_builtin で作成されている。KBUILD_VMLINUX_INIT や KBUILD_VMLINUX_MAIN で指定したファイル(この変数はトップディレクトリの Makefile で指定されている)をアーカイブにしている。つまりカーネルのコア部分だと思われる。
scripts/link-vmlinux.sh
archive_builtin()
{
info AR built-in.a
rm -f built-in.a;
${AR} rcsTP${KBUILD_ARFLAGS} built-in.a \
${KBUILD_VMLINUX_INIT} \
${KBUILD_VMLINUX_MAIN}
}
ということで最終的には、カーネル本体と、そこから生成したシンボル情報が連結される。
ここまでの流れを確認するために、カーネルビルドしてみる。次のようなメッセージが出力され、アセンブリを出力してからオブジェクトファイルを作成している様子がわかる。
++ basename .tmp_kallsyms1.o .o
+ local afile=.tmp_kallsyms1.S
+ scripts/kallsyms --all-symbols --absolute-percpu --base-relative
+ nm -n .tmp_vmlinux1
+ gcc -D__ASSEMBLY__ -fno-PIE -DCC_HAVE_ASM_GOTO -m64 -DCONFIG_AS_CFI=1 -DCONFIG_AS_CFI_SIGNAL_FRAME=1 -DCONFIG_AS_CFI_SECTIONS=1 -DCONFIG_AS_FXSAVEQ=1 -DCONFIG_AS_SSSE3=1 -DCONFIG_AS_CRC32=1 -DCONFIG_AS_AVX=1 -DCONFIG_AS_AVX2=1 -DCONFIG_AS_AVX512=1 -DCONFIG_AS_SHA1_NI=1 -DCONFIG_AS_SHA256_NI=1 -Wa,-gdwarf-2 -mfentry -DCC_USING_FENTRY -nostdinc -isystem /usr/lib/gcc/x86_64-redhat-linux/8/include -I./arch/x86/include -I./arch/x86/include/generated -I./include -I./arch/x86/include/uapi -I./arch/x86/include/generated/uapi -I./include/uapi -I./include/generated/uapi -include ./include/linux/kconfig.h -D__KERNEL__ -c -o .tmp_kallsyms1.o .tmp_kallsyms1.S
+ vmlinux_link .tmp_kallsyms1.o .tmp_vmlinux2
+ local lds=./arch/x86/kernel/vmlinux.lds
+ local objects
+ '[' x86 '!=' um ']'
+ objects='--whole-archive built-in.a --no-whole-archive --start-group lib/lib.a arch/x86/lib/lib.a --end-group .tmp_kallsyms1.o'
+ ld -m elf_x86_64 -z max-page-size=0x200000 --emit-relocs --build-id -X -o .tmp_vmlinux2 -T ./arch/x86/kernel/vmlinux.lds --whole-archive built-in.a --no-whole-archive --start-group lib/lib.a arch/x86/lib/lib.a --end-group .tmp_kallsyms1.o
+ kallsyms .tmp_vmlinux2 .tmp_kallsyms2.o
+ info KSYM .tmp_kallsyms2.o
+ '[' silent_ '!=' silent_ ']'
+ local kallsymopt
+ '[' -n y ']'
+ kallsymopt=' --all-symbols'
+ '[' -n y ']'
+ kallsymopt=' --all-symbols --absolute-percpu'
+ '[' -n y ']'
+ kallsymopt=' --all-symbols --absolute-percpu --base-relative'
+ local 'aflags=-D__ASSEMBLY__ -fno-PIE -DCC_HAVE_ASM_GOTO -m64 -DCONFIG_AS_CFI=1 -DCONFIG_AS_CFI_SIGNAL_FRAME=1 -DCONFIG_AS_CFI_SECTIONS=1 -DCONFIG_AS_FXSAVEQ=1 -DCONFIG_AS_SSSE3=1 -DCONFIG_AS_CRC32=1 -DCONFIG_AS_AVX=1 -DCONFIG_AS_AVX2=1 -DCONFIG_AS_AVX512=1 -DCONFIG_AS_SHA1_NI=1 -DCONFIG_AS_SHA256_NI=1 -Wa,-gdwarf-2 -mfentry -DCC_USING_FENTRY -nostdinc -isystem /usr/lib/gcc/x86_64-redhat-linux/8/include -I./arch/x86/include -I./arch/x86/include/generated -I./include -I./arch/x86/include/uapi -I./arch/x86/include/generated/uapi -I./include/uapi -I./include/generated/uapi -include ./include/linux/kconfig.h -D__KERNEL__ '
++ basename .tmp_kallsyms2.o .o
+ local afile=.tmp_kallsyms2.S
+ scripts/kallsyms --all-symbols --absolute-percpu --base-relative
+ nm -n .tmp_vmlinux2
+ gcc -D__ASSEMBLY__ -fno-PIE -DCC_HAVE_ASM_GOTO -m64 -DCONFIG_AS_CFI=1 -DCONFIG_AS_CFI_SIGNAL_FRAME=1 -DCONFIG_AS_CFI_SECTIONS=1 -DCONFIG_AS_FXSAVEQ=1 -DCONFIG_AS_SSSE3=1 -DCONFIG_AS_CRC32=1 -DCONFIG_AS_AVX=1 -DCONFIG_AS_AVX2=1 -DCONFIG_AS_AVX512=1 -DCONFIG_AS_SHA1_NI=1 -DCONFIG_AS_SHA256_NI=1 -Wa,-gdwarf-2 -mfentry -DCC_USING_FENTRY -nostdinc -isystem /usr/lib/gcc/x86_64-redhat-linux/8/include -I./arch/x86/include -I./arch/x86/include/generated -I./include -I./arch/x86/include/uapi -I./arch/x86/include/generated/uapi -I./include/uapi -I./include/generated/uapi -include ./include/linux/kconfig.h -D__KERNEL__ -c -o .tmp_kallsyms2.o .tmp_kallsyms2.S
++ /bin/sh ./scripts/file-size.sh .tmp_kallsyms1.o
+ size1=1706424
++ /bin/sh ./scripts/file-size.sh .tmp_kallsyms2.o
+ size2=1706424
+ '[' 1706424 -ne 1706424 ']'
+ '[' -n '' ']'
+ info LD vmlinux
+ '[' silent_ '!=' silent_ ']'
+ vmlinux_link .tmp_kallsyms2.o vmlinux
+ local lds=./arch/x86/kernel/vmlinux.lds
+ local objects
+ '[' x86 '!=' um ']'
+ objects='--whole-archive built-in.a --no-whole-archive --start-group lib/lib.a arch/x86/lib/lib.a --end-group .tmp_kallsyms2.o'
+ ld -m elf_x86_64 -z max-page-size=0x200000 --emit-relocs --build-id -X -o vmlinux -T ./arch/x86/kernel/vmlinux.lds --whole-archive built-in.a --no-whole-archive --start-group lib/lib.a arch/x86/lib/lib.a --end-group .tmp_kallsyms2.o
+ '[' -n y ']'
+ info SORTEX vmlinux
+ '[' silent_ '!=' silent_ ']'
+ sortextable vmlinux
+ ./scripts/sortextable vmlinux
+ info SYSMAP System.map
+ '[' silent_ '!=' silent_ ']'
+ mksysmap vmlinux System.map
+ /bin/sh ./scripts/mksysmap vmlinux System.map
+ '[' -n y ']'
+ mksysmap .tmp_vmlinux2 .tmp_System.map
+ /bin/sh ./scripts/mksysmap .tmp_vmlinux2 .tmp_System.map
+ cmp -s System.map .tmp_System.map
またアセンブリは、次のようなファイルが作られていた。
.tmp_kallsyms2.S
.section .rodata, "a"
.globl kallsyms_offsets
ALGN
kallsyms_offsets:
.long 0
.long 0
.long 0x8000
.long 0x9000
.long 0xc000
.long 0xd000
...
.globl kallsyms_relative_base
ALGN
kallsyms_relative_base:
PTR _text - 0
.globl kallsyms_num_syms
ALGN
kallsyms_num_syms:
PTR 101473
.globl kallsyms_names
ALGN
kallsyms_names:
.byte 0x09, 0x41, 0xb0, 0x71, 0x0e, 0x63, 0xda, 0xdf, 0xc3, 0x6e
.byte 0x08, 0x41, 0xff, 0x70, 0xcb, 0x7f, 0x0e, 0x72, 0x74
.byte 0x09, 0x41, 0x7f, 0x5f, 0xf1, 0x8f, 0x67, 0xf6, 0xb8, 0xf7
.byte 0x07, 0x41, 0x7f, 0x5f, 0x87, 0xee, 0x72, 0x7
...
話をさかのぼって/proc/kallsyms
の出力部分に戻る。アドレス・種類・シンボルをそれぞれ設定するが、これらは先述した通り、上記のアセンブリに含まれる値。なので、種類・シンボル名についても処理はほぼ同じ。今回はここまでにする。
まとめ
カーネルビルド時にカーネルを nm した結果をもとにシンボル情報を作成する。
そのほかの利用
シンボル情報を作成するまでに、けっこう手間がかかっている。これが /proc/kallsyms だけのために用意されてるとは思えないので、他に利用している箇所を探してみた。
するとカーネル内部で#include <linux/kallsyms.h>
というヘッダーが定義されていた。この実体はkernel/kallsyms.c
に格納された先述のシンボル情報を活用している。カーネル内で kprobe が呼び出したり、kdb が呼び出したり、スケジューラがログ吐くために呼び出したりしてる。
System.map
ちなみにカーネルビルドの中で、 System.map も作成されている。System.map は scripts/link-vmlinux.sh から scripts/mksysmap を呼びだして作成するが、これは nm コマンドと grep コマンドの小さなスクリプトになっている。(そのわりにコメントは 40 行くらい書いてある)
scripts/link-vmlinux.sh
vmlinux_link "${kallsymso}" vmlinux
if [ -n "${CONFIG_BUILDTIME_EXTABLE_SORT}" ]; then
info SORTEX vmlinux
sortextable vmlinux
fi
info SYSMAP System.map
mksysmap vmlinux System.map
scripts/mksysmap
$NM -n $1 | grep -v '\( [aNUw] \)\|\(__crc_\)\|\( \$[adt]\)\|\( .L\)' > $2
参考書籍
最後に
動作中のプログラム(カーネル)が自分自身のシンボル情報を持っている、そして実行中にアクセスできる、というのが面白いなと思いました。普通は別のプロセスが ELF のシンボルテーブルセクションなどにアクセスする気がしますが、OS より下はいないですもんね。OS として自分の状況を把握・報告しないといけないのでこういう仕組みが必要なんだろうなと思いますが…ユーザ空間のアプリケーションと比べると特殊ですね…自分で自分のケツを拭くスタイル。
また今回カーネルビルド時の処理を調査してたらシェルスクリプトで書かれていることに気づきました。昔は Makefile だったようですが、次のパッチでシェルスクリプトに書き直されてます。
参考 move link of vmlinux to a script [LWN.net]
書き直された理由が面白くて、「負債も溜まってきたし Makefile が普通の人間には読めないから」。カーネルの世界のビルド職人…やばそうですね
The part of the top-level Makefile that links the final vmlinux has accumulated a lot of cruft over times and the makefile code is unreadable for any normal human.