Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I just tried to make this work and couldn't. I might be missing something? gcc 7.5, Ubuntu 18.04, x86_64. The third-from-bottom line is two libraries showing the same address.

  $ cat *.h *.c
  // common.h
  #include <stdio.h>
  typedef int (*printf_ptr)(const char * restrict format, ...);
  printf_ptr firstA();
  printf_ptr firstB();
  printf_ptr secondA();
  printf_ptr secondB();
  
  // first.c
  #include "common.h"
  printf_ptr firstA() { return &printf; }
  printf_ptr firstB() { return &printf; }
  
  // second.c
  #include "common.h"
  printf_ptr secondA() { return &printf; }
  printf_ptr secondB() { return &printf; }
  
  // third.c
  #include "common.h"
  int main() {
   printf("%p %p %p %p\n", firstA(), firstB(), secondA(), secondB());
   firstA()("hello world %d\n", 1);
   secondA()("hello world %d\n", 2);
   return 0;
  }
  
  $ make
  gcc -fPIC -c *.c
  gcc -shared -o first.so first.o -lc
  gcc -shared -o second.so second.o -lc
  gcc -o atprintf third.o ./first.so ./second.so
  ./atprintf
  0x7f6d24e08f70 0x7f6d24e08f70 0x7f6d24e08f70 0x7f6d24e08f70
  hello world 1
  hello world 2


Ahh... I was wrong! There's one detail I wasn't aware of: if you take the address of a dynamic function, it appears to disable ELF lazy dynamic symbol binding for that function, so that it's guaranteed the GOT entry has been resolved to the global function address before main() is entered. It then just unconditionally uses the GOT entry as the function address.

If we print the address of main and never take the address of printf, we get lazy symbol resolution for printf like I expected:

    #include <stdio.h>
    int main() {
        printf("main is at %p\n", main);
        return 0;
    }
    
objdump --section .plt -d a.out shows 2 PLT entries:

    0000000000001020 <.plt>:
        1020:       ff 35 e2 2f 00 00       pushq  0x2fe2(%rip)        # 4008 <_GLOBAL_OFFSET_TABLE_+0x8>
        1026:       ff 25 e4 2f 00 00       jmpq   *0x2fe4(%rip)        # 4010 <_GLOBAL_OFFSET_TABLE_+0x10>
        102c:       0f 1f 40 00             nopl   0x0(%rax)

    0000000000001030 <printf@plt>:
        1030:       ff 25 e2 2f 00 00       jmpq   *0x2fe2(%rip)        # 4018 <printf@GLIBC_2.2.5>
        1036:       68 00 00 00 00          pushq  $0x0
        103b:       e9 e0 ff ff ff          jmpq   1020 <.plt>
That jmpq *0x2fe2(%rip) in the second PLT entry is an indirect jump through the prnitf GOT entry. The printf GOT entry is actually initialized to point right back at the pushq $0x0 inside pritf@plt. That pushes 0 on the stack so the dynamic symbol resolution knows which GOT entry it's lazily resolving. The jmpq 1020 <.plt> jumps to the first PLT entry, which then uses the 0 at the top of the stack to know it's resolving the printf GOT entry.

But, if we ever actually take the address of printf, then printf ceases to be a lazily bound ELF symbol:

    #include <stdio.h>
    int main() {
        printf("printf is at %p\n", printf);
        return 0;
    }
Note that we lose the dynamic resolution PLT stub for printf (objdump --section .plt -d a.out):

    0000000000001020 <.plt>:
        1020:       ff 35 e2 2f 00 00       pushq  0x2fe2(%rip)        # 4008 <_GLOBAL_OFFSET_TABLE_+0x8>
        1026:       ff 25 e4 2f 00 00       jmpq   *0x2fe4(%rip)        # 4010 <_GLOBAL_OFFSET_TABLE_+0x10>
        102c:       0f 1f 40 00             nopl   0x0(%rax)

And gcc -O2 -S main.c shows it's just unconditionally loading the GOT entry (printf@GOTPCREL) to use as the function address. (Note the dissasembly shows printf@PLT, but objdump doesn't show this PLT entry. I guess the linker does some link-time optimization there to remove the actual PLT entry.)

    main:
    .LFB11:
        .cfi_startproc
        subq    $8, %rsp
        .cfi_def_cfa_offset 16
        movq    printf@GOTPCREL(%rip), %rsi
        leaq    .LC0(%rip), %rdi
        xorl    %eax, %eax
        call    printf@PLT

https://refspecs.linuxfoundation.org/ELF/zSeries/lzsabi0_zSe... (section named Function Addresses) mentions that things (at least for IBM zSeries) basically work as I originally expected, but further vaguely mentions some special steps are taken to make function addresses compare as expected. It doesn't specifically mention disabling ELF dynamic symbol resolution.*


Thanks for the detail! I did manage to see different values for &printf with DLLs on Windows. I was inspired by a long-ago port of a codebase to a newer Visual C++; the third-party binary DLLs kept importing the old C runtime and using its malloc/free, which complicated things.

I put a __declspec(dllexport) on everything in my header file then (abridged):

  C:\Users\andrew\Desktop\winlink>cl /LD first.c
  Microsoft (R) C/C++ Optimizing Compiler Version 19.16.27035 for x64
  Copyright (C) Microsoft Corporation.  All rights reserved.

  /out:first.dll
  /dll
  /implib:first.lib
  first.obj
     Creating library first.lib and object first.exp
  
  C:\Users\andrew\Desktop\winlink>cl /LD second.c
  
  C:\Users\andrew\Desktop\winlink>cl third.c first.lib second.lib
  
  C:\Users\andrew\Desktop\winlink>third.exe
  00007FFE38411080 00007FFE38411080 00007FFE382F1080 00007FFE382F1080
  hello world 1
  hello world 2

If I statically link the three modules together on Windows, then I don't see the different pointers.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: