← home

What is pragma weak (GCC)?

From this very interesting article about linking of executables I found out that there exist a #pragma weak foofunction directive. It tells linker to handle the function as weakly defined. What it means is that if linker fails to find definition (implementation) of the function it will skip the function and won't show any errors. In this note I will demonstrate how does it work.

Example Application

Let's create a simple example:

#pragma weak debug
extern void debug(char*);
void (*debugfunc)(char*) = debug;

int main(){
    if(debugfunc){
        (*debugfunc)("hello");
    }

    return 0;
}

At line 2 we define debug function with an extern keyword. That means that this function can be defined in any of the application source files (or in any object file). The next line contains pointer (named debugfunc) to this function.

In the main() in the if-condition we check that if debugfunc have anything but zero. If it is not zero we call it, otherwise application terminates.

Next, let's create second file with the implementation of debug function. It is very very simple:

#include "stdio.h"

void debug(char * str){
    printf("[DEBUG] %s\n", str);
}

What does #pragma weak does?

See what happens if we compile main.c only:

andrew at andrew-laptop in /tmp/mainfun
➔ gcc -Wall -o app main.c

andrew at andrew-laptop in /tmp/mainfun
➔ ./app

Nothing =). But if we compile debug.c and then link it together with newly compiled main.c, then:

andrew at andrew-laptop in /tmp/mainfun
➔ gcc -Wall -c main.c

andrew at andrew-laptop in /tmp/mainfun
➔ gcc -Wall -c debug.c

andrew at andrew-laptop in /tmp/mainfun
➔ gcc -Wall -o app main.o debug.o

andrew at andrew-laptop in /tmp/mainfun
➔ ./app
[DEBUG] hello

Note that to compile files separately without linking you need to use -c argument.

In the first case linker couldn't find implementation for debug() and replace it with zero. Therefore, in all places where we reference debug() we get zero. As debugfunc pointer points to the debug() and also contains 0 it's not called. In the second case linker found implementation for debug() and treat it as a normal function. In this case debugfunc is pointing to the debug() (non-zero address in memory) and therefore will be called.

Look inside

Let's look what is really happening in the binaries and if it is true what is described in the previous paragraph. Firstly, let's compile both examples as two separated binaries for further comparison:

andrew at andrew-laptop in /tmp/mainfun
➔ gcc -Wall -o app main.o

andrew at andrew-laptop in /tmp/mainfun
➔ gcc -Wall -o appd main.o debug.o

Next let's look what is the difference between them. With nm utility we can see that in the first binary there no debug symbol (reference to the function) at all.

andrew at andrew-laptop in /tmp/mainfun
➔ nm app | grep debug
0000000000004028 D debugfunc

andrew at andrew-laptop in /tmp/mainfun
➔ nm appd | grep debug
0000000000001160 T debug
0000000000004030 D debugfunc

Actually, there quite a lot of small discrepancies between two binaries. You can look on the differences with the following command:

andrew at andrew-laptop in /tmp/mainfun
➔ vimdiff <(objdump -d app) <(objdump -d appd)

Disassembly of the main function should be similar to this:

0000000000001119 <main>:
    1119:       55                      push   %rbp
    111a:       48 89 e5                mov    %rsp,%rbp
    111d:       48 8b 05 04 2f 00 00    mov    0x2f04(%rip),%rax    # 4028 <debugfunc>
    1124:       48 85 c0                test   %rax,%rax
    1127:       74 10                   je     1139 <main+0x20>
    1129:       48 8b 05 f8 2e 00 00    mov    0x2ef8(%rip),%rax    # 4028 <debugfunc>
    1130:       48 8d 3d cd 0e 00 00    lea    0xecd(%rip),%rdi     # 2004 <_IO_stdin_used+0x4>
    1137:       ff d0                   callq  *%rax
    1139:       b8 00 00 00 00          mov    $0x0,%eax
    113e:       5d                      pop    %rbp
    113f:       c3                      retq

The first two instruction are used to save address of the previous stack frame and switch to the frame local the current function (for more info see [4]). The third one moves value located at address 0x4028 to the %rax register. This, in turn, is used in the following test instruction which checks if it is equals to zero and if so it sets ZF flag to 1 [5]. The next instruction je jumps to the address 1139 if ZF flag is equal to 1. The 1139 address is the end of the function (return 0;).

The 0x4028 address is equal to 0x2f04 + %rip (0x1124 - the address of the next instruction). The %rip is used for relative referencing (see [6]).

What is located at address 0x4028? As we know that it is global static variable it should be somewhere in the .data section. We can find it out with following command:

 objdump -s -j .data app
app:     file format elf64-x86-64

Contents of section .data:
 4018 00000000 00000000 20400000 00000000  ........ @......
 4028 00000000 00000000                    ........

As you can see it is all zeros. So, ZF will be 0 and je will jump to 1139.

In opposite case if there was something at 4028 then %rax wasn't zero, ZF was set to zero and je didn't jump. Even though the second binary has a little bit different addresses the main() is completely the same.

0000000000001139 <main>:
    1139:       55                      push   %rbp
    113a:       48 89 e5                mov    %rsp,%rbp
    113d:       48 8b 05 ec 2e 00 00    mov    0x2eec(%rip),%rax    # 4030 <debugfunc>
    1144:       48 85 c0                test   %rax,%rax
    1147:       74 10                   je     1159 <main+0x20>
    1149:       48 8b 05 e0 2e 00 00    mov    0x2ee0(%rip),%rax    # 4030 <debugfunc>
    1150:       48 8d 3d ad 0e 00 00    lea    0xead(%rip),%rdi     # 2004 <_IO_stdin_used+0x4>
    1157:       ff d0                   callq  *%rax
    1159:       b8 00 00 00 00          mov    $0x0,%eax
    115e:       5d                      pop    %rbp
    115f:       c3                      retq

The address of the to which debugfunc points is 0x4030. Again, let's use objdump to see what is in the .data section:

 objdump -s -j .data appd

appd:     file format elf64-x86-64

Contents of section .data:
 4020 00000000 00000000 28400000 00000000  ........(@......
 4030 60110000 00000000                    `.......

Thoughts

Personally, I don't think that it is a good approach to base your debugging function on this directive. As initially it was created for backward compatibility and general definition of function in the libraries (function overriding) [2], [3]. But if you are using third party library with weak function you can define your for debugging.

I search through some GNU project and other projects for the use-cases of this directive. It seems like it is not commonly used. Only in some specific cases, for example, in pthreadlib and musl-libc. However, I think it is very convenient and interesting way to disable/enable debugging or development features. Maybe in future I will find a way how to use it.

References


Hey👋 I'm Andrey. In this blog I post my personal short tutorials or interesting technical notes. Over the day I work as a Software Engineer developing and testing Linux filesystems. I use free software mainly #NixOS #Neovim #Kitty. Btw I use NixOS. Subscribe for updates on:

telegram@alberand@mas.totwitter