This article has been translated from Chinese by ChatGPT, and the wording may not be entirely accurate.

Addressing Linux Kernel Section GC Failure Issues - Part 2

Overview

This article is part of the Addressing Linux Kernel Section GC Failure Issues series.

The previous articles introduced the usage and principles of Section GC, as well as the Section GC failure problem in the Linux kernel.

To thoroughly solve this problem, we need to ensure that .pushsection can correctly establish reference relationships, avoiding the use of forced retention to prevent dependency inversion.

After reviewing documentation and community discussions, we have summarized two methods that can manually establish reference relationships.

Solutions

Section Group

In the same section group, if one section is retained, then all sections in the group will be kept. This is a powerful tool for solving the Section GC failure.

The section group to which a section belongs can be specified when creating the section in assembly.

.section name , "flags"G, @type, GroupName[, linkage]

Or use the flag ? to follow the parent section’s group.

.section name , "flags"?

The current idea is:

  1. Add the section group attribute to the parent function.
  2. Use flag ? to make Pushed Sections follow the parent Section’s group.

This way, both sections can be kept simultaneously.

There are two methods to complete the first step of adding the section group attribute to a C function.

  • Method 1:

Use the assembly directive .attach_to_group name to add a group to a section after it has been created.

int fun1() {
  asm(".attach_to_group \"MyGroup\"");

  asm(".pushsection .test,\"ax?\",@progbits\n"
      ".string \"hello\"\n"
      ".popsection");
  return 0;
}

int main() {
  fun1();
  return 0;
}
$ riscv64-linux-gnu-gcc -ffunction-sections -Wl,--gc-sections example.c -c
$ riscv64-linux-gnu-readelf -g example.o

group section [    1] `.group' [MyGroup] contains 2 sections:
   [Index]    Name
   [    5]   .text.fun1
   [    6]   .test
$ riscv64-linux-gnu-gcc -ffunction-sections  -Wl,--gc-sections,--print-gc-sections example.c
ld: removing unused section '.rodata.cst4' in file '/usr/riscv64-linux-gnu/usr/lib/Scrt1.o'
ld: removing unused section '.riscv.attributes' in file '/usr/lib/gcc/riscv64-linux-gnu/12.2.0/crti.o'
ld: removing unused section '.riscv.attributes' in file '/usr/lib/gcc/riscv64-linux-gnu/12.2.0/crtn.o'

.test was not GCed and was retained along with the group’s .text.main.

  • Method 2:

This method is not recommended.

Write assembly within __attribute__((section("section-name"))), include flag and GroupName, then manually add # to truncate the subsequent assembly code generated by the compiler in a manner similar to SQL injection.

int __attribute__((section(".text.fun1,\"axG\",@progbits,\"MyGroup\" #")))fun1()
{
    asm(".pushsection .test1,\"axG\",@progbits,\"MyGroup\"\n"
    "        .string \"hello\"\n"
    ".popsection");
    return 0;
}

The generated assembly code is:

.section .text.fun1,"axG",@progbits,"MyGroup" #,"ax",@progbits

The assembler actually processes:

.section .text.fun1,"axG",@progbits,"MyGroup"

Flawed Solutions

As mentioned in the previous article, __ex_table’s .pushsection introduced a problem of dependency inversion, which is defined as follows:

// arch/riscv/include/asm/asm-extable.h:14

#define __ASM_EXTABLE_RAW(insn, fixup, type, data)  \
  ".pushsection __ex_table, \"a\"\n"        \
  ".balign 4\n"             \
  ".long     (( insn) - .)\n"      \
  ".long     (( fixup) - .)\n"      \
  ".short     ( type)\n"          \
  ".short     ( data)\n"          \
  ".popsection\n"

To retain it with the parent function in a section group, the simplest method is to add .attach_to_group "GroupName" directly into the macro, and add ? to the flag in .pushsection:

#define __ASM_EXTABLE_RAW(insn, fixup, type, data)  \
  ".attach_to_group GroupName" \
  ".pushsection __ex_table, \"a?\"\n"       \
  ".balign 4\n"             \
  ".long     (( insn) - .)\n"      \
  ".long     (( fixup) - .)\n"      \
  ".short     ( type)\n"          \
  ".short     ( data)\n"          \
  ".popsection\n"

This way, no matter where this macro is expanded, a group will be added for the Section Pusher, and the .pushsection will be in the same group.

Each Section Pusher should be in a different group; otherwise, if one is retained, other unused Section Pushers will also be retained.

What’s more, this macro may expand multiple times within a function, meaning the Section Pusher has called .pushsection several times, resulting in multiple executions of .attach_to_group, and linkers will produce a warning. We hope to execute .attach_to_group only once within a function.

  • If only the filename is used as GroupName and ifdefs assist in determining whether the current Section Pusher is already in a group, if not, then .attach_to_group. Then, all functions in one file would be added to a group and retained or GCed together, which is not satisfactory.
  • If the filename and line number are used as GroupName, the macro may still expand multiple times within a function and cannot determine whether the current Section Pusher is within a group.

Therefore, we need a function-level unique string to serve as GroupName. Then we could use ifdefs to determine whether the current Section Pusher is already in a group.

Function name is the most readily accessible method, but neither __func__ nor __FUNCTION__ are macros; they are determined during compilation. Therefore, we cannot use the function name as GroupName. At present, we are left with a format similar to the filename and line number.

#define ___PASTE(a,b) a##b
#define __PASTE(a,b) ___PASTE(a,b)

#ifndef __UNIQUE_ID
# define __UNIQUE_ID __PASTE(__PASTE(__COUNTER__, _), __LINE__)
#endif

#define __ASM_EXTABLE_RAW(insn, fixup, type, data)  \
  ".attach_to_group "__stringify(__UNIQUE_ID_Extable)"\n" \
  ".pushsection __ex_table, \"a?\"\n"      \
  ".balign 4\n"             \
  ".long     (( insn) - .)\n"      \
  ".long     (( fixup) - .)\n"      \
  ".short     ( type)\n"          \
  ".short     ( data)\n"          \
  ".popsection\n"

This way, compilation will produce warnings:

Warning: section .text.main already has a group (GroupName)

The kernel community does not accept code with warnings, and the linker does not provide options to turn off this warning, so although this solution solves the problem, it can not be merged into the mainline.

Reviewing the documentation for as, you can see the definitions of .pushsection and .section.

.pushsection name [, subsection] [, "flags"[, @type[,arguments]]]
.section name [, "flags"[, @type[,flag_specific_arguments]]]

One of the flags meets our requirements:

o
section references a symbol defined in another section (the linked-to section) in the same file.

If flags contains the o flag, then the type argument must be present along with an additional field like this:

.section name,"flags"o,@type,SymbolName|SectionIndex

We can use this flag to manually specify that this section refers to the Section Pusher and establishes a reference.

void unused_function() {
    return;
}

int main() {
    asm("Section_Pusher_Symbol:\n");
    asm(".pushsection .should_not_GC,\"ao\",@progbits,Section_Pusher_Symbol\n"
        ".popsection");
    return 0;
}
$ riscv64-linux-gnu-gcc -ffunction-sections -Wl,--gc-sections,--print-gc-sections example_shf.c
ld: removing unused section '.rodata.cst4' in file '/usr/lib/gcc-cross/riscv64-linux-gnu/11/../../../../riscv64-linux-gnu/lib/Scrt1.o'
ld: removing unused section '.riscv.attributes' in file '/usr/lib/gcc-cross/riscv64-linux-gnu/11/crti.o'
ld: removing unused section '.text.unused_function' in file '/tmp/cceocy7f.o'
ld: removing unused section '.riscv.attributes' in file '/usr/lib/gcc-cross/riscv64-linux-gnu/11/crtn.o'

In this code, we created a label Section_Pusher_Symbol in main(), then used the o flag in .pushsection, specified to that symbol.

As seen, after adding the o flag, the pushed section was not GCed, achieving the project goal.

Verification in the Linux Kernel

Linux Lab has integrated Patches based on the above two solutions. Switch to the section-gc branch of Linux Lab to verify.

git checkout section-gc

Enable the dse feature and compile:

make test b=riscv64/virt f=dse LINUX=v6.6-rc2

Check the number of system calls retained:

$ nm build/riscv64/virt/linux/v6.6-rc2/vmlinux | grep "T __riscv_sys" | grep -v sys_ni_syscall | wc -l
40

The result shows that many system calls were trimmed, verifying the feasibility of the solutions.

Conclusion

In this article, we proposed two methods to solve the Linux Kernel Section GC failure problem. They allow all sections to establish the correct dependency relations without side effects, avoiding the use of KEEP, and provide more information to linkers.

References