Fix a Build Breakage on Inline Function

7 minute read

I’ve done a lot of work on u-boot-2010.09 for MINI2440 to make development process more comfortable, unfortunately, the archived u-boot images were deleted by accident, so I try to do a fresh build, also the arm-linux-gcc previously used was also lost (coincidence?), and I decided to use the one I get from Linaro’s website, the version is 7.5.0:

$ arm-linux-gnueabi-gcc -v
Using built-in specs.
COLLECT_GCC=arm-linux-gnueabi-gcc
COLLECT_LTO_WRAPPER=/usr/local/arm/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabi/bin/../libexec/gcc/arm-linux-gnueabi/7.5.0/lto-wrapper
Target: arm-linux-gnueabi
Configured with: '/home/tcwg-buildslave/workspace/tcwg-make-release_0/snapshots/gcc.git~linaro-7.5-2019.12/configure' SHELL=/bin/bash --with-mpc=/home/tcwg-buildslave/workspace/tcwg-make-release_0/_build/builds/destdir/x86_64-unknown-linux-gnu --with-mpfr=/home/tcwg-buildslave/workspace/tcwg-make-release_0/_build/builds/destdir/x86_64-unknown-linux-gnu --with-gmp=/home/tcwg-buildslave/workspace/tcwg-make-release_0/_build/builds/destdir/x86_64-unknown-linux-gnu --with-gnu-as --with-gnu-ld --disable-libmudflap --enable-lto --enable-shared --without-included-gettext --enable-nls --with-system-zlib --disable-sjlj-exceptions --enable-gnu-unique-object --enable-linker-build-id --disable-libstdcxx-pch --enable-c99 --enable-clocale=gnu --enable-libstdcxx-debug --enable-long-long --with-cloog=no --with-ppl=no --with-isl=no --disable-multilib --with-float=soft --with-mode=thumb --with-tune=cortex-a9 --with-arch=armv7-a --enable-threads=posix --enable-multiarch --enable-libstdcxx-time=yes --enable-gnu-indirect-function --with-build-sysroot=/home/tcwg-buildslave/workspace/tcwg-make-release_0/_build/sysroots/arm-linux-gnueabi --with-sysroot=/home/tcwg-buildslave/workspace/tcwg-make-release_0/_build/builds/destdir/x86_64-unknown-linux-gnu/arm-linux-gnueabi/libc --enable-checking=release --disable-bootstrap --enable-languages=c,c++,fortran,lto --build=x86_64-unknown-linux-gnu --host=x86_64-unknown-linux-gnu --target=arm-linux-gnueabi --prefix=/home/tcwg-buildslave/workspace/tcwg-make-release_0/_build/builds/destdir/x86_64-unknown-linux-gnu
Thread model: posix
gcc version 7.5.0 (Linaro GCC 7.5-2019.12)

I expect it build without any problem, as it is proved to be working perfectly date back to year 2015 (the last commit I submitted).

But life is not always in sunshine, that’s the way we learn from our experience, the following failures are shown during the build process:

#1
board.c:112:6: error: ‘coloured_LED_init’ aliased to external symbol ‘__coloured_LED_init’
 void coloured_LED_init (void) __attribute__((weak, alias("__coloured_LED_init")));

#2
main.c:51:6: error: ‘show_boot_progress’ aliased to external symbol ‘__show_boot_progress’
 void show_boot_progress (int val) __attribute__((weak, alias("__show_boot_progress")));
      ^~~~~~~~~~~~~~~~~~

#3
drivers/mtd/nand/libnand.a(nand_base.o): In function `__raw_writesw':
/tmp/u-boot-2010.09/include/asm/io.h:86: multiple definition of `__raw_writesw'
arch/arm/cpu/arm920t/s3c24x0/libs3c24x0.a(interrupts.o):/tmp/u-boot-2010.09/include/asm/io.h:86: first defined here

The first two breakages are the same, and the last one is a little bit different, and is also inline related, at first I don’t want to spending too much time on it, so I googled and found the following two patches in the list:

The error messages are not the same, I assume it is because the GCC version is different, these two patches fixed the first two issues, the first one without any explanation, and the second one sent by Emil Medve tells the reason as follows:

The ‘inline’ is conflicting with the semantic of ‘weak’ attribute and with the way the show_boot_progress() function is used.

It’s inline related, but I have no idea about what the confliction is, as inline is used to direct GCC to make calls to that function faster, for my case these two functions is not used, so removing inline is acceptable for me, but if they are used, the speed advantage is lost, so do we have other solutions for this kind of issues.

Finding Nemo

Let’s get started with the keyword inline, according to the GCC manual, there are two implementations regarding this scenario with different semantics, the third one is for c++, which will not be discussed here:

  • -std=gnu89
  • -std=c99, -std=c11, -std=gnu99 or -std=gnu11

At this point I guess the GCC will always use the latest available standard which makes the two versions of GCC I used using different standard by default, my guess is right, the default standard for GCC varies between versions, the two versions used here, it is gnu89 and gnu11 respectively:

GCC Standards
4.3.2 gnu89
7.5.0 gnu11

Hint: search for The default, if no C language dialect options are given.

The GCC manual (both version) also described that there are two cases which both types of inlining behave similarly, the first case is as its example, when the inline keyword is used on a static function:

static inline int
inc (int *a)
{
    return (*a)++;
}

This is the relevant code in U-Boot is:

void inline __show_boot_progress (int val) {}
void show_boot_progress (int val) __attribute__((weak, alias("__show_boot_progress")));

So try to define show_boot_progress as static to see the result (#2), both version works without any problem:

# 7.5.0
$ arm-linux-gnueabi-gcc -c board.c -o board.o && arm-linux-gnueabi-nm board.o
00000000 t __show_boot_progress
00000000 W show_boot_progress

# 4.3.2
$ arm-linux-gcc -c board.c -o board.o && arm-linux-nm board.o
00000000 t __show_boot_progress
00000000 W show_boot_progress

The other one is when a function is first declared without using the inline keyword and then is defined with inline, like this:

extern int inc (int *a);
inline int
inc (int *a)
{
    return (*a)++;
}

This also works for both version (see #3)

# 7.5.0
$ arm-linux-gnueabi-gcc -c board.c -o board.o && arm-linux-gnueabi-nm board.o
00000000 T __show_boot_progress
00000000 W show_boot_progress
# 4.3.2
$ arm-linux-gcc -c board.c -o board.o && arm-linux-nm board.o
00000000 T __show_boot_progress
00000000 W show_boot_progress

As this is caused by using different standard in GCC, here comes another solution (#4), use gnu89 for new GCC:

$ arm-linux-gnueabi-gcc -std=gnu89 -c board.c -o board.o && arm-linux-gnueabi-nm board.o

Finding Dory

As for now, there are three different solutions for this issue, but the question is still unanswered:

  • what’s the difference between the two standard on non-static inline functions?
  • What is the confliction between inline and weak semantics?

The Wikipedia Inline function page tells the differences between inline semantic between these two standard.

In C99, a function defined inline will never emit an externally visible function. and a function defined extern inline will always emit an externally visible function.

In gnu89, a function defined inline will always emit an externally visible function. and a function defined extern inline will never emit an externally visible function.

Taking the following for example:

cat foo.c
inline void foo() {}
# gnu99, nm shows nothing:
$ arm-linux-gnueabi-gcc -c foo.c -o foo.o && arm-linux-gnueabi-nm foo.o

# gnu89
$ arm-linux-gcc -c foo.c -o foo.o && arm-linux-nm foo.o
00000000 T foo

We cannot find symbol foo, let’s make it more clearer with below example:

void inline foo(int val) { }

int main() { foo(0xdeee); }

The results of readlef and nm are the same, foo does not exist:

$ arm-linux-gnueabi-readelf -s foo.o

Symbol table '.symtab' contains 12 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS foo.c
     2: 00000000     0 SECTION LOCAL  DEFAULT    1
     3: 00000000     0 SECTION LOCAL  DEFAULT    3
     4: 00000000     0 SECTION LOCAL  DEFAULT    4
     5: 00000000     0 NOTYPE  LOCAL  DEFAULT    1 $a
     6: 00000024     0 NOTYPE  LOCAL  DEFAULT    1 $d
     7: 00000000     0 SECTION LOCAL  DEFAULT    6
     8: 00000000     0 SECTION LOCAL  DEFAULT    5
     9: 00000000     0 SECTION LOCAL  DEFAULT    7
    10: 00000000    40 FUNC    GLOBAL DEFAULT    1 main
    11: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND foo
$ arm-linux-gnueabi-nm -s foo.o
         U foo
00000000 T main

Now let’s see the extern case:

extern inline void foo() {}
# gnu99
$ arm-linux-gnueabi-gcc -c foo.c -o foo.o && arm-linux-gnueabi-nm foo.o
00000000 T foo
# gnu89 shows nothing
$ arm-linux-gcc -c foo.c -o foo.o && arm-linux-nm foo.o

This is exactly what’s described in Wikipedia.

The meaning of letters before the symbol are described in nm manual:

If lowercase, the symbol is usually local; if uppercase, the symbol is global (external). There are however a few lowercase symbols that are shown for special global symbols (“u”, “v” and “w”).

For the second keyword weak, the only information seems useful in GCC manual is this:

The weak attribute causes the declaration to be emitted as a weak symbol rather than a global.

The last is alias, the alias attribute is just as it indicates, it give a symbol another name, according to section 6.31.1 Common Function Attributes of GCC manual, the target (__show_boot_progress for this case) must be in the same translation unit.

This seems conflict with the inline in C99, and matches the error message, rebuild board.c with weak attribute remove report the same error, so this is the root cause of this problem.

ARM Compiler toolchain Compiler Reference also confirms this requirement:

Where a function is defined in the current translation unit, the alias call is replaced by a call to the function, and the alias is emitted alongside the original name.

Where a function is not defined in the current translation unit, the alias call is replaced by a call to the real function.

Where a function is defined as static, the function name is replaced by the alias name and the function is declared external if the alias name is declared external.

Terms referred in this section:

  • Translation unit roughly consists of a source file after it has been processed by the C preprocessor, meaning that header files listed in #include directives are literally included, sections of code within #ifndef may be included, and macros have been expanded.

Solutions

#1 The one in the list:

void __show_boot_progress (int val) {}
void show_boot_progress (int val) __attribute__((weak, alias("__show_boot_progress")));

#2 Use inline on static function

static void inline __show_boot_progress (int val) {}
void show_boot_progress (int val) __attribute__((weak, alias("__show_boot_progress")));

#3 Declare function without inline, then define it with inline:

extern void __show_boot_progress (int val);
void inline __show_boot_progress (int val) { }
void show_boot_progress (int val) __attribute__((weak, alias("__show_boot_progress")));

#4 Change GCC to use gnu89 standard:

$ arm-linux-gnueabi-gcc -std=gnu89 -c board.c -o board.o && arm-linux-gnueabi-nm board.o
00000000 T __show_boot_progress
00000000 W show_boot_progress

# To only affect inlining, use -fgnu89-inline
$ arm-linux-gnueabi-gcc -fgnu89-inline -c board.c -o board.o && arm-linux-gnueabi-nm board.o
00000000 T __show_boot_progress
00000000 W show_boot_progress

References