61

So everyone probably knows that glibc's /lib/libc.so.6 can be executed in the shell like a normal executable in which cases it prints its version information and exits. This is done via defining an entry point in the .so. For some cases it could be interesting to use this for other projects too. Unfortunately, the low-level entry point you can set by ld's -e option is a bit too low-level: the dynamic loader is not available so you cannot call any proper library functions. glibc for this reason implements the write() system call via a naked system call in this entry point.

My question now is, can anyone think of a nice way how one could bootstrap a full dynamic linker from that entry point so that one could access functions from other .so's?

3

4 Answers 4

57

Update 2: see Andrew G Morgan's slightly more complicated solution which does work for any GLIBC (that solution is also used in libc.so.6 itself (since forever), which is why you can run it as ./libc.so.6 (it prints version info when invoked that way)).

Update 1: this no longer works with newer GLIBC versions:

./a.out: error while loading shared libraries: ./pie.so: cannot dynamically load position-independent executable

Original answer from 2009:

Building your shared library with -pie option appears to give you everything you want:

/* pie.c */
#include <stdio.h>
int foo()
{
  printf("in %s %s:%d\n", __func__, __FILE__, __LINE__);
  return 42; 
}
int main() 
{ 
  printf("in %s %s:%d\n", __func__, __FILE__, __LINE__);
  return foo(); 
}


/* main.c */
#include <stdio.h>

extern int foo(void);
int main() 
{ 
  printf("in %s %s:%d\n", __func__, __FILE__, __LINE__);
  return foo(); 
}


$ gcc -fPIC -pie -o pie.so pie.c -Wl,-E
$ gcc main.c ./pie.so


$ ./pie.so
in main pie.c:9
in foo pie.c:4
$ ./a.out
in main main.c:6
in foo pie.c:4
$

P.S. glibc implements write(3) via system call because it doesn't have anywhere else to call (it is the lowest level already). This has nothing to do with being able to execute libc.so.6.

Sign up to request clarification or add additional context in comments.

4 Comments

I found that the -shared option might prevent this from working, and give you a segmentation fault. You have to make sure the -shared option is not there or put it before -pie so it gets ignored.
So this is as simple as adding a main() and -pie to my shared library?
This no longer works with newer GLIBC: ./a.out: error while loading shared libraries: ./pie.so: cannot dynamically load position-independent executable.
It looks like there is an answer that works now.
18

I have been looking to add support for this to pam_cap.so, and found this question. As @EmployedRussian notes in a follow-up to their own post, the accepted answer stopped working at some point. It took a while to figure out how to make this work again, so here is a worked example.

This worked example involves 5 files to show how things work with some corresponding tests.

First, consider this trivial program (call it empty.c):

Compiling it, we can see how it resolves the dynamic symbols on my system as follows:

$ gcc -o empty empty.c
$ objcopy --dump-section .interp=/dev/stdout empty ; echo
/lib64/ld-linux-x86-64.so.2
$ DL_LOADER=/lib64/ld-linux-x86-64.so.2

That last line sets a shell variable for use later.

Here are the two files that build my example shared library:

and

Updates:

  • 2021-11-13: The forced alignment is to help __i386__ code be SSE compatible - without it we get hard to debug glibc SIGSEGV crashes.

  • 2025-03-22: The _IO_stdin_used weak definition works around another hard to debug glibc issue.

We can compile and run it as follows:

$ gcc -fPIC -shared -o multi.so -DDL_LOADER="\"${DL_LOADER}\"" multi.c -Wl,-e,multi_main
$ ./multi.so
called from multi.c
$ echo $?
42

So, this is a .so that can be executed as a stand alone binary. Next, we validate that it can be loaded as shared object.

That is we dynamically load the shared-object and run a function from it:

$ gcc -o opener opener.c -ldl
$ ./opener
called from opener.c

Finally, we link against this shared object:

Where we compile and run it as follows:

$ gcc main.c -o main multi.so
$ LD_LIBRARY_PATH=./ ./main
called from main.c

Note: because multi.so isn't in a standard system library location, we need to override where the runtime looks for the shared object file with the LD_LIBRARY_PATH environment variable.

2025-05-04 For C++ users (at least using g++), as noted by @Haydentech in the comments, trying to compile the above files generates a compile time error:

multi.c:13:11: error: weak declaration of ‘_IO_stdin_used’ must be public
   13 | const int _IO_stdin_used __attribute__((weak)) = 131073;
      |           ^~~~~~~~~~~~~~

This is because of a known bug with the compiler. However, it is not the only problem you will encounter. C++ linking mangles symbols, so the name you give things isn't always the name they are linked with. This leads to problems with the entry point not working. For these cases, you will need the following workarounds. Replace the multi.h file content with this:

/* multi.h - C++ compatible */
#ifdef __cplusplus
extern "C" {
#endif

void multi_main(void);
void multi(const char *caller);

#ifdef __cplusplus
}
#endif

and replace multi.c file content with this:

/* multi.c - C++ compatible */
#include <stdio.h>
#include <stdlib.h>
#include "multi.h"

void multi(const char *caller) {
    printf("called from %s\n", caller);
}

#ifdef __GLIBC__
extern const int _IO_stdin_used;
#ifdef __cplusplus
/* Workaround for https://gcc.gnu.org/bugzilla/show_bug.cgi?id=83271 */
extern
#endif /* def __cplusplus */
const int _IO_stdin_used __attribute__((weak)) = 131073;
#endif /* def __GLIBC__ */

#ifdef __cplusplus
extern "C" {
#endif /* def __cplusplus */

__attribute__((force_align_arg_pointer))
void multi_main(void) {
    multi(__FILE__);
    exit(42);
}

#ifdef __cplusplus
}
#endif /* def __cplusplus */

const char dl_loader[] __attribute__((section(".interp"))) =
    DL_LOADER ;

These changes should still permit the files to compile with gcc but they make the source code even harder to read, so I've opted to explain them as a postscript.

11 Comments

this needs multi_main(void) ? cannot it not get the command lien arguments?
Take a look at execable.h in the libcap sources. This parses the /proc/self/cmdline file to construct the command line arguments. The execable.c file in the same directory shows how it is used.
Actually I really need the argv pointer, not the arguments themselves. *argv is mutable, you can write to it and change the arguments. It is very peculiar, writing to argv also changes /proc/self/cmdline. But /proc/self/cmdline is readonly, so you cannot write to that
But now I found the solution, %rsp points to argc/argv/env.
But now I worry about how stable this is. Libc seems to have a lot of initialization code and this method seems to skip that? Perhaps there will be a libc update one day and then these multi programs will just crash at startup? (I know libc 2.34 broke FreePascal due to the initialization code changes. They were preparing a new FreePascal compiler release in 2021 and the libc change was one of the reasons they still have been able to complete the release )
|
1

I suppose you'd have your ld -e point to an entry point which would then use the dlopen() family of functions to find and bootstrap the rest of the dynamic linker. Of course you'd have to ensure that dlopen() itself was either statically linked or you might have to implement enough of your own linker stub to get at it (using system call interfaces such as mmap() just as libc itself is doing.

None of that sounds "nice" to me. In fact just the thought of reading the glibc sources (and the ld-linux source code, as one example) enough to assess the size of the job sounds pretty hoary to me. It might also be a portability nightmare. There may be major differences between how Linux implements ld-linux and how the linkages are done under OpenSolaris, FreeBSD, and so on. (I don't know).

Comments

0

For those who, like me, were bothered that support for this was dropped from glibc, I figured a (dirty) solution by looking at the original patch. It checks for DF_1_PIE in FLAGS_1, so I'm patching the binary to drop the DF_1_PIE from FLAGS_1 and that's sufficient to make ld.so happy. I did it in the following patch for one of my tools: http://git.formilux.org/?p=people/willy/nousr.git;a=commitdiff;h=f4ffb101

Please note that this involves binary patching using sed, so it only works empirically because I noticed that all other flags were zero, but will likely not work if other flags are set, maybe on other archs, possibly on 32-bit and will certainly not work on big endian. But it allows me to support the utility again:

$ LD_PRELOAD=./nousr.so ls
Makefile  README  nousr.c  nousr.so*  nousr.so-p1  nousr.so-p2  nousr.so-p2-patched  nousr.so-p3  patch-it.txt
$ ./nousr.so ls
Makefile  README  nousr.c  nousr.so  nousr.so-p1  nousr.so-p2  nousr.so-p2-patched  nousr.so-p3  patch-it.txt

It could of course be improved to match other blocks and replicate them, but I don't care for now. For those who want to try it by hand, build a shared object named foo.so and apply the commands:

gcc -shared -fPIC -c foo.c
gcc -pie -o foo.so foo.o
# LD_PRELOAD=./foo.so ls
# ERROR: ld.so: object './foo.so' from LD_PRELOAD cannot be preloaded (cannot open shared object file): ignored.

set -- $(objdump -j .dynamic -h foo.so | fgrep .dynamic)
size=$3; ofs=$6
dd if=foo.so of=foo.so-p1 bs=1 count=$((0x$ofs))
dd if=foo.so of=foo.so-p2 bs=1 skip=$((0x$ofs)) count=$((0x$size))
dd if=foo.so of=foo.so-p3 bs=1 skip=$((0x$ofs+0x$size))
sed -e 's,\xfb\xff\xff\x6f\x00\x00\x00\x00\x00\x00\x00\x08,\xfb\xff\xff\x6f\x00\x00\x00\x00\x00\x00\x00\x00,g' < foo.so-p2 > foo.so-p2-patched 
cat foo.so{-p1,-p2-patched,-p3} > foo2.so
chmod 755 foo2.so
# LD_PRELOAD=./foo.so ls
# foo.c foo.o foo.so foo2.so ...

Hoping this can help someone, as I found this extremely annoying, and none of the proposed solutions suited me :-/

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.