diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a697cf..60bf145 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,16 +5,17 @@ name: CI on: push: branches: - - master + - master + - add-osx-support paths-ignore: - - 'LICENSE.txt' - - '**.md' - - 'specs/**' + - 'LICENSE.txt' + - '**.md' + - 'specs/**' pull_request: paths-ignore: - - 'LICENSE.txt' - - '**.md' - - 'specs/**' + - 'LICENSE.txt' + - '**.md' + - 'specs/**' jobs: Baseline: strategy: @@ -22,51 +23,62 @@ jobs: matrix: os: [ubuntu-20.04, ubuntu-22.04, ubuntu-latest] cc: [[gcc, g++], [clang, clang++]] - py: [python3.6, python3.7, python3] # We need f-strings so 3.6+ + py: ['3.6', '3.7', '3.10'] exclude: - os: ubuntu-22.04 - py: python3.6 + py: '3.6' - os: ubuntu-latest - py: python3.6 + py: '3.6' - os: ubuntu-22.04 - cc: [clang, clang++] # Disable due to https://sourceware.org/bugzilla/show_bug.cgi?id=28981 + cc: [clang, clang++] - os: ubuntu-latest - cc: [clang, clang++] # Ditto + cc: [clang, clang++] runs-on: ${{ matrix.os }} env: CC: ${{ matrix.cc[0] }} CXX: ${{ matrix.cc[1] }} - PYTHON: ${{ matrix.py }} + PYTHON: python3 steps: - - uses: actions/checkout@v3 - - name: Install deps - run: | - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install ${PYTHON} - sudo apt-get install ${PYTHON}-pip || true - - name: Run tests - run: scripts/travis.sh - PyPy: # Can't test this in matrix, pypy package has different names in old distros + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.py }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.py }} + - name: Install deps + run: | + python3 -m pip install lief + - name: Run tests + run: scripts/travis.sh + + PyPy: runs-on: ubuntu-latest env: PYTHON: pypy3 steps: - - uses: actions/checkout@v3 - - name: Install deps - run: sudo apt-get install ${PYTHON} - - name: Run tests - run: scripts/travis.sh + - uses: actions/checkout@v3 + - name: Set up PyPy + uses: actions/setup-python@v4 + with: + python-version: 'pypy3.9' + - name: Install deps + run: | + pypy3 -m pip install lief + - name: Run tests + run: scripts/travis.sh + Pylint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Install deps - run: sudo apt-get install pylint - - name: Run tests - run: | - pylint implib-gen.py - pylint scripts/ld + - uses: actions/checkout@v3 + - name: Install deps + run: | + sudo apt-get install pylint python3-pip + sudo python3 -m pip install lief + - name: Run tests + run: | + pylint implib-gen.py + pylint scripts/ld + Coverage: runs-on: ubuntu-latest environment: secrets @@ -75,170 +87,214 @@ jobs: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} PYTHON: 'coverage run -a' steps: - - uses: actions/checkout@v3 - - name: Install deps - run: | - sudo apt-get install python3 python3-pip - sudo python3 -mpip install codecov - - name: Run tests - run: scripts/travis.sh - - name: Upload coverage - run: | - for t in tests/*; do - if test -d $t -a -f $t/.coverage; then - (cd $t && coverage xml) - fi - done - codecov --required + - uses: actions/checkout@v3 + - name: Install deps + run: | + sudo apt-get install python3 python3-pip + sudo python3 -m pip install codecov lief + - name: Run tests + run: scripts/travis.sh + - name: Upload coverage + run: | + for t in tests/*; do + if test -d $t -a -f $t/.coverage; then + (cd $t && coverage xml) + fi + done + codecov --required + avx: runs-on: ubuntu-latest env: CFLAGS: -mavx steps: - - uses: actions/checkout@v3 - - name: Run tests - run: scripts/travis.sh -# avx2: -# runs-on: ubuntu-latest -# env: -# CFLAGS: -mavx2 -mavx512f -# steps: -# - uses: actions/checkout@v3 -# - name: Run tests -# run: scripts/travis.sh + - uses: actions/checkout@v3 + - name: Install deps + run: | + sudo apt-get install python3-pip + sudo python3 -m pip install lief + - name: Run tests + run: scripts/travis.sh musl: runs-on: ubuntu-latest env: CC: musl-gcc steps: - - uses: actions/checkout@v3 - - name: Install deps - run: sudo apt-get update && sudo apt-get install musl-tools - - name: Run tests - run: scripts/travis.sh - # TODO: test on llvm-libc when it supports dlfcn + - uses: actions/checkout@v3 + - name: Install deps + run: | + sudo apt-get update && sudo apt-get install musl-tools python3-pip + sudo python3 -m pip install lief + - name: Run tests + run: scripts/travis.sh x86: runs-on: ubuntu-latest env: ARCH: i386-linux-gnueabi steps: - - uses: actions/checkout@v3 - - name: Install deps - run: sudo apt-get update && sudo apt-get install gcc-multilib g++-multilib - - name: Run tests - run: scripts/travis.sh + - uses: actions/checkout@v3 + - name: Install deps + run: | + sudo apt-get update && sudo apt-get install gcc-multilib g++-multilib python3-pip + sudo python3 -m pip install lief + - name: Run tests + run: scripts/travis.sh + arm-arm: runs-on: ubuntu-latest env: ARCH: arm-linux-gnueabi CFLAGS: -marm steps: - - uses: actions/checkout@v3 - - name: Install deps - run: sudo apt-get update && sudo apt-get install qemu-user gcc-arm-linux-gnueabi g++-arm-linux-gnueabi binutils-arm-linux-gnueabi libc6-armel-cross libc6-dev-armel-cross - - name: Run tests - run: scripts/travis.sh + - uses: actions/checkout@v3 + - name: Install deps + run: | + sudo apt-get update && sudo apt-get install qemu-user gcc-arm-linux-gnueabi g++-arm-linux-gnueabi binutils-arm-linux-gnueabi libc6-armel-cross libc6-dev-armel-cross python3-pip + sudo python3 -m pip install lief + - name: Run tests + run: scripts/travis.sh + arm-thumb: runs-on: ubuntu-latest env: ARCH: arm-linux-gnueabi CFLAGS: -mthumb steps: - - uses: actions/checkout@v3 - - name: Install deps - run: sudo apt-get update && sudo apt-get install qemu-user gcc-arm-linux-gnueabi g++-arm-linux-gnueabi binutils-arm-linux-gnueabi libc6-armel-cross libc6-dev-armel-cross - - name: Run tests - run: scripts/travis.sh + - uses: actions/checkout@v3 + - name: Install deps + run: | + sudo apt-get update && sudo apt-get install qemu-user gcc-arm-linux-gnueabi g++-arm-linux-gnueabi binutils-arm-linux-gnueabi libc6-armel-cross libc6-dev-armel-cross python3-pip + sudo python3 -m pip install lief + - name: Run tests + run: scripts/travis.sh + armhf-arm: runs-on: ubuntu-latest env: ARCH: arm-linux-gnueabihf CFLAGS: -marm steps: - - uses: actions/checkout@v3 - - name: Install deps - run: sudo apt-get update && sudo apt-get install qemu-user gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf binutils-arm-linux-gnueabihf libc6-armhf-cross libc6-dev-armhf-cross - - name: Run tests - run: scripts/travis.sh + - uses: actions/checkout@v3 + - name: Install deps + run: | + sudo apt-get update && sudo apt-get install qemu-user gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf binutils-arm-linux-gnueabihf libc6-armhf-cross libc6-dev-armhf-cross python3-pip + sudo python3 -m pip install lief + - name: Run tests + run: scripts/travis.sh + armhf-thumb: runs-on: ubuntu-latest env: ARCH: arm-linux-gnueabihf CFLAGS: -mthumb steps: - - uses: actions/checkout@v3 - - name: Install deps - run: sudo apt-get update && sudo apt-get install qemu-user gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf binutils-arm-linux-gnueabihf libc6-armhf-cross libc6-dev-armhf-cross - - name: Run tests - run: scripts/travis.sh + - uses: actions/checkout@v3 + - name: Install deps + run: | + sudo apt-get update && sudo apt-get install qemu-user gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf binutils-arm-linux-gnueabihf libc6-armhf-cross libc6-dev-armhf-cross python3-pip + sudo python3 -m pip install lief + - name: Run tests + run: scripts/travis.sh + aarch64: runs-on: ubuntu-latest env: ARCH: aarch64-linux-gnueabi steps: - - uses: actions/checkout@v3 - - name: Install deps - run: sudo apt-get update && sudo apt-get install qemu-user gcc-aarch64-linux-gnu g++-aarch64-linux-gnu binutils-aarch64-linux-gnu libc6-arm64-cross libc6-dev-arm64-cross - - name: Run tests - run: scripts/travis.sh + - uses: actions/checkout@v3 + - name: Install deps + run: | + sudo apt-get update && sudo apt-get install qemu-user gcc-aarch64-linux-gnu g++-aarch64-linux-gnu binutils-aarch64-linux-gnu libc6-arm64-cross libc6-dev-arm64-cross python3-pip + sudo python3 -m pip install lief + - name: Run tests + run: scripts/travis.sh + mipsel: runs-on: ubuntu-latest env: ARCH: mipsel-linux-gnu steps: - - uses: actions/checkout@v3 - - name: Install deps - run: sudo apt-get update && sudo apt-get install qemu-user gcc-mipsel-linux-gnu g++-mipsel-linux-gnu binutils-mipsel-linux-gnu libc6-mipsel-cross libc6-dev-mipsel-cross - - name: Run tests - run: scripts/travis.sh + - uses: actions/checkout@v3 + - name: Install deps + run: | + sudo apt-get update && sudo apt-get install qemu-user gcc-mipsel-linux-gnu g++-mipsel-linux-gnu binutils-mipsel-linux-gnu libc6-mipsel-cross libc6-dev-mipsel-cross python3-pip + sudo python3 -m pip install lief + - name: Run tests + run: scripts/travis.sh + mips64el: runs-on: ubuntu-latest env: ARCH: mips64el-linux-gnu steps: - - uses: actions/checkout@v3 - - name: Install deps - run: sudo apt-get update && sudo apt-get install qemu-user gcc-mips64el-linux-gnuabi64 g++-mips64el-linux-gnuabi64 binutils-mips64el-linux-gnuabi64 libc6-mips64el-cross libc6-dev-mips64el-cross - - name: Run tests - run: scripts/travis.sh + - uses: actions/checkout@v3 + - name: Install deps + run: | + sudo apt-get update && sudo apt-get install qemu-user gcc-mips64el-linux-gnuabi64 g++-mips64el-linux-gnuabi64 binutils-mips64el-linux-gnuabi64 libc6-mips64el-cross libc6-dev-mips64el-cross python3-pip + sudo python3 -m pip install lief + - name: Run tests + run: scripts/travis.sh + powerpc64: runs-on: ubuntu-latest env: ARCH: powerpc64-linux-gnu steps: - - uses: actions/checkout@v3 - - name: Install deps - run: sudo apt-get update && sudo apt-get install qemu-user gcc-powerpc64-linux-gnu g++-powerpc64-linux-gnu binutils-powerpc64-linux-gnu libc6-ppc64-powerpc-cross libc6-dev-ppc64-powerpc-cross - - name: Run tests - run: scripts/travis.sh + - uses: actions/checkout@v3 + - name: Install deps + run: | + sudo apt-get update && sudo apt-get install qemu-user gcc-powerpc64-linux-gnu g++-powerpc64-linux-gnu binutils-powerpc64-linux-gnu libc6-ppc64-powerpc-cross libc6-dev-ppc64-powerpc-cross python3-pip + sudo python3 -m pip install lief + - name: Run tests + run: scripts/travis.sh + powerpc64le: runs-on: ubuntu-latest env: ARCH: powerpc64le-linux-gnu steps: - - uses: actions/checkout@v3 - - name: Install deps - run: sudo apt-get update && sudo apt-get install qemu-user gcc-powerpc64le-linux-gnu g++-powerpc64le-linux-gnu binutils-powerpc64le-linux-gnu libc6-ppc64el-cross libc6-dev-ppc64el-cross - - name: Run tests - run: scripts/travis.sh + - uses: actions/checkout@v3 + - name: Install deps + run: | + sudo apt-get update && sudo apt-get install qemu-user gcc-powerpc64le-linux-gnu g++-powerpc64le-linux-gnu binutils-powerpc64le-linux-gnu libc6-ppc64el-cross libc6-dev-ppc64el-cross python3-pip + sudo python3 -m pip install lief + - name: Run tests + run: scripts/travis.sh + riscv64: runs-on: ubuntu-latest env: ARCH: riscv64-linux-gnu steps: - - uses: actions/checkout@v3 - - name: Install deps - run: sudo apt-get update && sudo apt-get install qemu-user gcc-riscv64-linux-gnu g++-riscv64-linux-gnu binutils-riscv64-linux-gnu libc6-riscv64-cross libc6-dev-riscv64-cross - - name: Run tests - run: scripts/travis.sh + - uses: actions/checkout@v3 + - name: Install deps + run: | + sudo apt-get update && sudo apt-get install qemu-user gcc-riscv64-linux-gnu g++-riscv64-linux-gnu binutils-riscv64-linux-gnu libc6-riscv64-cross libc6-dev-riscv64-cross python3-pip + sudo python3 -m pip install lief + - name: Run tests + run: scripts/travis.sh + freebsd: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: vmactions/freebsd-vm@v1 - with: - usesh: true - copyback: false + - uses: actions/checkout@v4 + - uses: vmactions/freebsd-vm@v1 + with: + usesh: true + copyback: false + run: | + scripts/install-freebsd-prereqs.sh + python3 -m ensurepip || true + python3 -m pip install lief + scripts/travis.sh + + osx: + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - name: Install deps run: | - scripts/install-freebsd-prereqs.sh - scripts/travis.sh + brew install python3 + python3 -m pip install lief --break-system-packages || python3 -m pip install lief + - name: Run tests + run: scripts/travis.sh \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0a3a22e..364fa4e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,7 @@ tests/*/*.log # GDB .gdb_history +.idea +.venv +pyproject.toml +uv.lock diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/README.md b/README.md index 6ab5c8e..cf34a28 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,6 @@ The tool does not transparently support all features of POSIX shared libraries. The tool also lacks the following important features: * symbol versions are not handled at all * keep fast paths of shims together to reduce I$ pressure -* support for macOS Finally, there are some minor TODOs in code. diff --git a/arch/README.md b/arch/linux/README.md similarity index 100% rename from arch/README.md rename to arch/linux/README.md diff --git a/arch/aarch64/config.ini b/arch/linux/aarch64/config.ini similarity index 100% rename from arch/aarch64/config.ini rename to arch/linux/aarch64/config.ini diff --git a/arch/aarch64/table.S.tpl b/arch/linux/aarch64/table.S.tpl similarity index 100% rename from arch/aarch64/table.S.tpl rename to arch/linux/aarch64/table.S.tpl diff --git a/arch/aarch64/trampoline.S.tpl b/arch/linux/aarch64/trampoline.S.tpl similarity index 100% rename from arch/aarch64/trampoline.S.tpl rename to arch/linux/aarch64/trampoline.S.tpl diff --git a/arch/arm/config.ini b/arch/linux/arm/config.ini similarity index 100% rename from arch/arm/config.ini rename to arch/linux/arm/config.ini diff --git a/arch/arm/table.S.tpl b/arch/linux/arm/table.S.tpl similarity index 100% rename from arch/arm/table.S.tpl rename to arch/linux/arm/table.S.tpl diff --git a/arch/arm/trampoline.S.tpl b/arch/linux/arm/trampoline.S.tpl similarity index 100% rename from arch/arm/trampoline.S.tpl rename to arch/linux/arm/trampoline.S.tpl diff --git a/arch/common/init.c.tpl b/arch/linux/common/init.c.tpl similarity index 100% rename from arch/common/init.c.tpl rename to arch/linux/common/init.c.tpl diff --git a/arch/e2k/README.md b/arch/linux/e2k/README.md similarity index 100% rename from arch/e2k/README.md rename to arch/linux/e2k/README.md diff --git a/arch/e2k/config.ini b/arch/linux/e2k/config.ini similarity index 100% rename from arch/e2k/config.ini rename to arch/linux/e2k/config.ini diff --git a/arch/e2k/table.S.tpl b/arch/linux/e2k/table.S.tpl similarity index 100% rename from arch/e2k/table.S.tpl rename to arch/linux/e2k/table.S.tpl diff --git a/arch/e2k/trampoline.S.tpl b/arch/linux/e2k/trampoline.S.tpl similarity index 100% rename from arch/e2k/trampoline.S.tpl rename to arch/linux/e2k/trampoline.S.tpl diff --git a/arch/i386/config.ini b/arch/linux/i386/config.ini similarity index 100% rename from arch/i386/config.ini rename to arch/linux/i386/config.ini diff --git a/arch/i386/table.S.tpl b/arch/linux/i386/table.S.tpl similarity index 100% rename from arch/i386/table.S.tpl rename to arch/linux/i386/table.S.tpl diff --git a/arch/i386/trampoline.S.tpl b/arch/linux/i386/trampoline.S.tpl similarity index 100% rename from arch/i386/trampoline.S.tpl rename to arch/linux/i386/trampoline.S.tpl diff --git a/arch/mips/config.ini b/arch/linux/mips/config.ini similarity index 100% rename from arch/mips/config.ini rename to arch/linux/mips/config.ini diff --git a/arch/mips/table.S.tpl b/arch/linux/mips/table.S.tpl similarity index 100% rename from arch/mips/table.S.tpl rename to arch/linux/mips/table.S.tpl diff --git a/arch/mips/trampoline.S.tpl b/arch/linux/mips/trampoline.S.tpl similarity index 100% rename from arch/mips/trampoline.S.tpl rename to arch/linux/mips/trampoline.S.tpl diff --git a/arch/mips64/config.ini b/arch/linux/mips64/config.ini similarity index 100% rename from arch/mips64/config.ini rename to arch/linux/mips64/config.ini diff --git a/arch/mips64/table.S.tpl b/arch/linux/mips64/table.S.tpl similarity index 100% rename from arch/mips64/table.S.tpl rename to arch/linux/mips64/table.S.tpl diff --git a/arch/mips64/trampoline.S.tpl b/arch/linux/mips64/trampoline.S.tpl similarity index 100% rename from arch/mips64/trampoline.S.tpl rename to arch/linux/mips64/trampoline.S.tpl diff --git a/arch/powerpc64/config.ini b/arch/linux/powerpc64/config.ini similarity index 100% rename from arch/powerpc64/config.ini rename to arch/linux/powerpc64/config.ini diff --git a/arch/powerpc64/table.S.tpl b/arch/linux/powerpc64/table.S.tpl similarity index 100% rename from arch/powerpc64/table.S.tpl rename to arch/linux/powerpc64/table.S.tpl diff --git a/arch/powerpc64/trampoline.S.tpl b/arch/linux/powerpc64/trampoline.S.tpl similarity index 100% rename from arch/powerpc64/trampoline.S.tpl rename to arch/linux/powerpc64/trampoline.S.tpl diff --git a/arch/powerpc64le/config.ini b/arch/linux/powerpc64le/config.ini similarity index 100% rename from arch/powerpc64le/config.ini rename to arch/linux/powerpc64le/config.ini diff --git a/arch/powerpc64le/table.S.tpl b/arch/linux/powerpc64le/table.S.tpl similarity index 100% rename from arch/powerpc64le/table.S.tpl rename to arch/linux/powerpc64le/table.S.tpl diff --git a/arch/powerpc64le/trampoline.S.tpl b/arch/linux/powerpc64le/trampoline.S.tpl similarity index 100% rename from arch/powerpc64le/trampoline.S.tpl rename to arch/linux/powerpc64le/trampoline.S.tpl diff --git a/arch/riscv64/config.ini b/arch/linux/riscv64/config.ini similarity index 100% rename from arch/riscv64/config.ini rename to arch/linux/riscv64/config.ini diff --git a/arch/riscv64/table.S.tpl b/arch/linux/riscv64/table.S.tpl similarity index 100% rename from arch/riscv64/table.S.tpl rename to arch/linux/riscv64/table.S.tpl diff --git a/arch/riscv64/trampoline.S.tpl b/arch/linux/riscv64/trampoline.S.tpl similarity index 100% rename from arch/riscv64/trampoline.S.tpl rename to arch/linux/riscv64/trampoline.S.tpl diff --git a/arch/x86_64/config.ini b/arch/linux/x86_64/config.ini similarity index 100% rename from arch/x86_64/config.ini rename to arch/linux/x86_64/config.ini diff --git a/arch/x86_64/table.S.tpl b/arch/linux/x86_64/table.S.tpl similarity index 100% rename from arch/x86_64/table.S.tpl rename to arch/linux/x86_64/table.S.tpl diff --git a/arch/x86_64/trampoline.S.tpl b/arch/linux/x86_64/trampoline.S.tpl similarity index 100% rename from arch/x86_64/trampoline.S.tpl rename to arch/linux/x86_64/trampoline.S.tpl diff --git a/arch/osx/aarch64/config.ini b/arch/osx/aarch64/config.ini new file mode 100644 index 0000000..b8766a2 --- /dev/null +++ b/arch/osx/aarch64/config.ini @@ -0,0 +1,3 @@ +[Arch] +PointerSize = 8 +SymbolReloc = R_AARCH64_ABS64, SYMBOLIC, RELATIVE diff --git a/arch/osx/aarch64/table.S.tpl b/arch/osx/aarch64/table.S.tpl new file mode 100644 index 0000000..b3be811 --- /dev/null +++ b/arch/osx/aarch64/table.S.tpl @@ -0,0 +1,67 @@ +/* + * Copyright 2018-2025 Yury Gribov + * + * The MIT License (MIT) + * + * Use of this source code is governed by MIT license that can be + * found in the LICENSE.txt file. + */ + +#define lr x30 +#define ip0 x16 + +.data + .globl __${lib_suffix}_tramp_table + .private_extern __${lib_suffix}_tramp_table + .p2align 3 +__${lib_suffix}_tramp_table: + .zero $table_size + + .text + + .globl __${lib_suffix}_tramp_resolve + .private_extern __${lib_suffix}_tramp_resolve + + .globl __${lib_suffix}_save_regs_and_resolve + .private_extern __${lib_suffix}_save_regs_and_resolve +__${lib_suffix}_save_regs_and_resolve: + .cfi_startproc + +#define PUSH_PAIR(reg1, reg2) stp reg1, reg2, [sp, #-16]!; .cfi_adjust_cfa_offset 16; .cfi_rel_offset reg1, 0; .cfi_rel_offset reg2, 8 +#define POP_PAIR(reg1, reg2) ldp reg1, reg2, [sp], #16; .cfi_adjust_cfa_offset -16; .cfi_restore reg2; .cfi_restore reg1 + +#define PUSH_WIDE_PAIR(reg1, reg2) stp reg1, reg2, [sp, #-32]!; .cfi_adjust_cfa_offset 32; .cfi_rel_offset reg1, 0; .cfi_rel_offset reg2, 16 +#define POP_WIDE_PAIR(reg1, reg2) ldp reg1, reg2, [sp], #32; .cfi_adjust_cfa_offset -32; .cfi_restore reg2; .cfi_restore reg1 + + // Save only arguments (and lr) + PUSH_PAIR(x0, x1) + PUSH_PAIR(x2, x3) + PUSH_PAIR(x4, x5) + PUSH_PAIR(x6, x7) + PUSH_PAIR(x8, lr) + + ldr x0, [sp, #80] // 16*5 + + PUSH_WIDE_PAIR(q0, q1) + PUSH_WIDE_PAIR(q2, q3) + PUSH_WIDE_PAIR(q4, q5) + PUSH_WIDE_PAIR(q6, q7) + + // Stack is aligned at 16 bytes + bl __${lib_suffix}_tramp_resolve + mov ip0, x0 + + POP_WIDE_PAIR(q6, q7) + POP_WIDE_PAIR(q4, q5) + POP_WIDE_PAIR(q2, q3) + POP_WIDE_PAIR(q0, q1) + + POP_PAIR(x8, lr) + POP_PAIR(x6, x7) + POP_PAIR(x4, x5) + POP_PAIR(x2, x3) + POP_PAIR(x0, x1) + + br lr + + .cfi_endproc diff --git a/arch/osx/aarch64/trampoline.S.tpl b/arch/osx/aarch64/trampoline.S.tpl new file mode 100644 index 0000000..32f99e3 --- /dev/null +++ b/arch/osx/aarch64/trampoline.S.tpl @@ -0,0 +1,42 @@ +/* + * Copyright 2018-2025 Yury Gribov + * + * The MIT License (MIT) + * + * Use of this source code is governed by MIT license that can be + * found in the LICENSE.txt file. + */ + + .globl _$sym + .p2align 4 +#ifndef IMPLIB_EXPORT_SHIMS + .private_extern _$sym +#endif +_$sym: + .cfi_startproc + +1: + // Load address via Mach-O PAGE/PAGEOFF directives + adrp ip0, (__${lib_suffix}_tramp_table + $offset)@PAGE + ldr ip0, [ip0, (__${lib_suffix}_tramp_table + $offset)@PAGEOFF] + + cbz ip0, 2f + + // Fast path + br ip0 + +2: + // Slow path + mov ip0, #($number & 0xffff) +#if $number > 0xffff + movk ip0, #($number >> 16), lsl #16 +#endif + stp ip0, lr, [sp, #-16]! + .cfi_adjust_cfa_offset 16 + .cfi_rel_offset lr, 8 + bl __${lib_suffix}_save_regs_and_resolve + ldp xzr, lr, [sp], #16 + .cfi_adjust_cfa_offset -16 + .cfi_restore lr + br ip0 + .cfi_endproc diff --git a/arch/osx/common/init.c.tpl b/arch/osx/common/init.c.tpl new file mode 100644 index 0000000..e98328c --- /dev/null +++ b/arch/osx/common/init.c.tpl @@ -0,0 +1,307 @@ +/* + * Copyright 2018-2025 Yury Gribov + * + * The MIT License (MIT) + * + * Use of this source code is governed by MIT license that can be + * found in the LICENSE.txt file. + */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE // For RTLD_DEFAULT +#endif + +#define HAS_DLOPEN_CALLBACK $has_dlopen_callback +#define HAS_DLSYM_CALLBACK $has_dlsym_callback +#define NO_DLOPEN $no_dlopen +#define LAZY_LOAD $lazy_load +#define THREAD_SAFE $thread_safe + +#include +#include +#include +#include +#include + +#ifdef __APPLE__ +#include + +#ifndef PTHREAD_BARRIER_SERIAL_THREAD +#define PTHREAD_BARRIER_SERIAL_THREAD 1 +#endif + +typedef struct { + pthread_mutex_t mutex; + pthread_cond_t cond; + unsigned int count; + unsigned int tripCount; +} pthread_barrier_t; + +static inline __attribute__((unused)) int pthread_barrier_init(pthread_barrier_t *barrier, const void *attr, unsigned int count) { + (void)attr; + if (count == 0) return -1; + if (pthread_mutex_init(&barrier->mutex, 0) < 0) return -1; + if (pthread_cond_init(&barrier->cond, 0) < 0) return -1; + barrier->tripCount = count; + barrier->count = 0; + return 0; +} + +static inline __attribute__((unused)) int pthread_barrier_destroy(pthread_barrier_t *barrier) { + pthread_mutex_destroy(&barrier->mutex); + pthread_cond_destroy(&barrier->cond); + return 0; +} + +static inline __attribute__((unused)) int pthread_barrier_wait(pthread_barrier_t *barrier) { + pthread_mutex_lock(&barrier->mutex); + ++(barrier->count); + if (barrier->count >= barrier->tripCount) { + barrier->count = 0; + pthread_cond_broadcast(&barrier->cond); + pthread_mutex_unlock(&barrier->mutex); + return PTHREAD_BARRIER_SERIAL_THREAD; + } else { + pthread_cond_wait(&barrier->cond, &barrier->mutex); + pthread_mutex_unlock(&barrier->mutex); + return 0; + } +} +#endif + +#if THREAD_SAFE +#include +#endif + +// Sanity check for ARM to avoid puzzling runtime crashes +#ifdef __arm__ +# if defined __thumb__ && ! defined __THUMB_INTERWORK__ +# error "ARM trampolines need -mthumb-interwork to work in Thumb mode" +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#define CHECK(cond, fmt, ...) do { \ + if(!(cond)) { \ + fprintf(stderr, "implib-gen: $load_name: " fmt "\n", ##__VA_ARGS__); \ + assert(0 && "Assertion in generated code"); \ + abort(); \ + } \ + } while(0) + +static void *lib_handle; +static int dlopened; + +#if ! NO_DLOPEN + +#if THREAD_SAFE + +// We need to consider two cases: +// - different threads calling intercepted APIs in parallel +// - same thread calling 2 intercepted APIs recursively +// due to dlopen calling library constructors +// (usually happens only under IMPLIB_EXPORT_SHIMS) + +// Current recursive mutex approach will deadlock +// if library constructor starts and joins a new thread +// which (directly or indirectly) calls another library function. +// Such situations should be very rare (although chances +// are higher when -DIMLIB_EXPORT_SHIMS are enabled). +// +// Similar issue is present in Glibc so hopefully it's +// not a big deal: // http://sourceware.org/bugzilla/show_bug.cgi?id=15686 +// (also google for "dlopen deadlock). + +static pthread_mutex_t mtx; +static int rec_count; + +static void init_lock(void) { + // We need recursive lock because dlopen will call library constructors + // which may call other intercepted APIs that will call load_library again. + // PTHREAD_RECURSIVE_MUTEX_INITIALIZER is not portable + // so we do it hard way. + + pthread_mutexattr_t attr; + CHECK(0 == pthread_mutexattr_init(&attr), "failed to init mutex"); + CHECK(0 == pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE), "failed to init mutex"); + + CHECK(0 == pthread_mutex_init(&mtx, &attr), "failed to init mutex"); +} + +static int lock(void) { + static pthread_once_t once = PTHREAD_ONCE_INIT; + CHECK(0 == pthread_once(&once, init_lock), "failed to init lock"); + + CHECK(0 == pthread_mutex_lock(&mtx), "failed to lock mutex"); + + return 0 == __sync_fetch_and_add(&rec_count, 1); +} + +static void unlock(void) { + __sync_fetch_and_add(&rec_count, -1); + CHECK(0 == pthread_mutex_unlock(&mtx), "failed to unlock mutex"); +} +#else +static int lock(void) { + return 1; +} +static void unlock(void) {} +#endif + +static int load_library(void) { + int publish = lock(); + + if (lib_handle) { + unlock(); + return publish; + } + +#if HAS_DLOPEN_CALLBACK + extern void *$dlopen_callback(const char *lib_name); + lib_handle = $dlopen_callback("$load_name"); + CHECK(lib_handle, "failed to load library '$load_name' via callback '$dlopen_callback'"); +#else + lib_handle = dlopen("$load_name", RTLD_LAZY | RTLD_GLOBAL); + CHECK(lib_handle, "failed to load library '$load_name' via dlopen: %s", dlerror()); +#endif + + // With (non-default) IMPLIB_EXPORT_SHIMS we may call dlopen more than once + // so dlclose it if we are not the first ones + if (__sync_val_compare_and_swap(&dlopened, 0, 1)) { + dlclose(lib_handle); + } + + unlock(); + + return publish; +} + +// Run dtor as late as possible in case library functions are +// called in other global dtors +// FIXME: this may crash if one thread is calling into library +// while some other thread executes exit(). It's no clear +// how to fix this besides simply NOT dlclosing library at all. +static void __attribute__((destructor(101))) unload_lib(void) { + if (dlopened) { + dlclose(lib_handle); + lib_handle = 0; + dlopened = 0; + } +} +#endif + +#if ! NO_DLOPEN && ! LAZY_LOAD +static void __attribute__((constructor(101))) load_lib(void) { + load_library(); +} +#endif + +// TODO: convert to single 0-separated string +static const char *const sym_names[] = { + $sym_names + 0 +}; + +#define SYM_COUNT (sizeof(sym_names)/sizeof(sym_names[0]) - 1) + +extern void *_${lib_suffix}_tramp_table[]; + +// Can be sped up by manually parsing library symtab... +void *_${lib_suffix}_tramp_resolve(size_t i) { + assert(i < SYM_COUNT); + + int publish = 1; + + void *h = 0; + int should_close_handle = 0; +#if NO_DLOPEN + // Library with implementations must have already been loaded. + if (lib_handle) { + // User has specified loaded library + h = lib_handle; + } else { + // User hasn't provided us the loaded library so search the global namespace. +# ifdef __APPLE__ + // On macOS, RTLD_NEXT from main executable often fails to find libraries loaded via dlopen. + // Try to get handle to the target library if it's already loaded (honoring NO_DLOPEN). + // We use RTLD_NOLOAD which is available on macOS 10.10+. + // Note that RTLD_NOLOAD on macOS increments refcount, so we must dlclose it later. + h = dlopen("$load_name", RTLD_LAZY | RTLD_NOLOAD | RTLD_GLOBAL); + if (h) should_close_handle = 1; +# endif + if (!h) { +# ifndef IMPLIB_EXPORT_SHIMS + // If shim symbols are hidden we should search + // for first available definition of symbol in library list + h = RTLD_DEFAULT; +# else + // Otherwise look for next available definition + h = RTLD_NEXT; +# endif + } + } +#else + publish = load_library(); + h = lib_handle; + CHECK(h, "failed to resolve symbol '%s', library failed to load", sym_names[i]); +#endif + + void *addr; +#if HAS_DLSYM_CALLBACK + extern void *$dlsym_callback(void *handle, const char *sym_name); + addr = $dlsym_callback(h, sym_names[i]); + CHECK(addr, "failed to resolve symbol '%s' via callback $dlsym_callback", sym_names[i]); +#else + // Dlsym is thread-safe so don't need to protect it. + addr = dlsym(h, sym_names[i]); +#endif + + if (should_close_handle) { + dlclose(h); + } + + CHECK(addr, "failed to resolve symbol '%s' via dlsym: %s", sym_names[i], dlerror()); + + if (publish) { + // Use atomic to please Tsan and ensure that preceeding writes + // in library ctors have been delivered before publishing address + (void)__sync_val_compare_and_swap(&_${lib_suffix}_tramp_table[i], 0, addr); + } + + return addr; +} + +// Below APIs are not thread-safe +// and it's not clear how make them such +// (we can not know if some other thread is +// currently executing library code). + +// Helper for user to resolve all symbols +void _${lib_suffix}_tramp_resolve_all(void) { + size_t i; + for(i = 0; i < SYM_COUNT; ++i) + _${lib_suffix}_tramp_resolve(i); +} + +// Allows user to specify manually loaded implementation library. +void _${lib_suffix}_tramp_set_handle(void *handle) { + // TODO: call unload_lib ? + lib_handle = handle; + dlopened = 0; +} + +// Resets all resolved symbols. This is needed in case +// client code wants to reload interposed library multiple times. +void _${lib_suffix}_tramp_reset(void) { + // TODO: call unload_lib ? + memset(_${lib_suffix}_tramp_table, 0, SYM_COUNT * sizeof(_${lib_suffix}_tramp_table[0])); + lib_handle = 0; + dlopened = 0; +} + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/implib-gen.py b/implib-gen.py index dfcaf75..e8bcdaf 100755 --- a/implib-gen.py +++ b/implib-gen.py @@ -1,720 +1,797 @@ #!/usr/bin/env python3 - -# Copyright 2017-2025 Yury Gribov -# -# The MIT License (MIT) -# -# Use of this source code is governed by MIT license that can be -# found in the LICENSE.txt file. - -""" -Generates static import library for POSIX shared library -""" - -import sys +from __future__ import annotations +import argparse +import bisect +import configparser +import itertools import os -import os.path import re -import subprocess -import argparse import string -import configparser +import sys +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from pathlib import Path +from typing import Callable, Any -me = os.path.basename(__file__) -root = os.path.dirname(__file__) - -def warn(msg): - """Emits a nicely-decorated warning.""" - sys.stderr.write(f'{me}: warning: {msg}\n') - -def error(msg): - """Emits a nicely-decorated error and exits.""" - sys.stderr.write(f'{me}: error: {msg}\n') - sys.exit(1) - -def run(args, stdin=''): - """Runs external program and aborts on error.""" - env = os.environ.copy() - # Force English language - env['LC_ALL'] = 'c' - try: - del env["LANG"] - except KeyError: - pass - with subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, env=env) as p: - out, err = p.communicate(input=stdin.encode('utf-8')) - out = out.decode('utf-8') - err = err.decode('utf-8') - if p.returncode != 0 or err: - error(f"{args[0]} failed with retcode {p.returncode}:\n{err}") - return out, err - -def is_binary_file(filename): - """Check if file is an ELF.""" - cmd = ['readelf', '-d', filename] - with subprocess.Popen(cmd, stdin=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL) as p: - p.communicate() - return p.returncode == 0 - -def make_toc(words, renames=None): - "Make an mapping of words to their indices in list" - renames = renames or {} - toc = {} - for i, n in enumerate(words): - name = renames.get(n, n) - toc[i] = name - return toc - -def parse_row(words, toc, hex_keys): - "Make a mapping from column names to values" - vals = {k: (words[i] if i < len(words) else '') for i, k in toc.items()} - for k in hex_keys: - if vals[k]: - vals[k] = int(vals[k], 16) - return vals - -def collect_syms(f): - """Collect ELF dynamic symtab.""" - - # --dyn-syms does not always work for some reason so dump all symtabs - out, _ = run(['readelf', '-sW', f]) - - toc = None - syms = [] - syms_set = set() - - for line in out.splitlines(): - line = line.strip() - - # Strip out strange markers in powerpc64le ELFs - line = re.sub(r'\[: [0-9]+\]', '', line) - - if not line: - # Next symtab - toc = None - continue - - words = re.split(r' +', line) - - if line.startswith('Num'): # Header? - if toc is not None: - error("multiple headers in output of readelf") - # Colons are different across readelf versions so get rid of them. - toc = make_toc(map(lambda n: n.replace(':', ''), words)) - elif toc is not None: - sym = parse_row(words, toc, ['Value']) - name = sym['Name'] - if not name: - continue - if name in syms_set: - continue - syms_set.add(name) - sym['Size'] = int(sym['Size'], 0) # Readelf is inconsistent about Size format - if '@' in name: - sym['Default'] = '@@' in name - name, ver = re.split(r'@+', name) - sym['Name'] = name - sym['Version'] = ver - else: - sym['Default'] = True - sym['Version'] = None - syms.append(sym) - - if toc is None: - error(f"failed to analyze symbols in {f}") - - return syms - -def collect_def_exports(filename): - """Reads exported symbols from .def file.""" - - syms = [] - - with open(filename, 'r') as f: - lines = f.readlines() - lines.reverse() - - while lines: - line = lines.pop().strip() - - if line != 'EXPORTS': - continue - - while lines: - line = lines.pop() - - if re.match(r'^\s*;', line): # Comment - continue - - # TODO: support renames - m = re.match(r'^\s+([A-Za-z0-9_]+)\s*$', line) - if m is None: - lines.append(line) - break - - sym = { - 'Name': m[1], - 'Bind': 'GLOBAL', - 'Type': 'FUNC', - 'Ndx': '0', - 'Default': True, - 'Version': None, - 'Size': 0, - } - syms.append(sym) - - if not syms: - warn(f"failed to locate symbols in {filename}") - - return syms - -def collect_relocs(f): - """Collect ELF dynamic relocs.""" - - out, _ = run(['readelf', '-rW', f]) - - toc = None - rels = [] - for line in out.splitlines(): - line = line.strip() - if not line: - toc = None - continue - if line == 'There are no relocations in this file.': - return [] - if re.match(r'^\s*Type[0-9]:', line): # Spurious lines for MIPS - continue - if re.match(r'^\s*Offset', line): # Header? - if toc is not None: - error("multiple headers in output of readelf") - words = re.split(r'\s\s+', line) # "Symbol's Name + Addend" - toc = make_toc(words) - elif re.match(r'^\s*r_offset', line): # FreeBSD header? - if toc is not None: - error("multiple headers in output of readelf") - words = re.split(r'\s\s+', line) # "st_name + r_addend" - toc = make_toc(words) - rename = { - 'r_offset' : 'Offset', - 'r_info' : 'Info', - 'r_type' : 'Type', - 'st_value' : 'Symbol\'s Value', - 'st_name + r_addend' : 'Symbol\'s Name + Addend', - } - toc = {idx : rename[name] for idx, name in toc.items()} - elif toc is not None: - line = re.sub(r' \+ ', '+', line) - words = re.split(r'\s+', line) - rel = parse_row(words, toc, ['Offset', 'Info']) - rels.append(rel) - # Split symbolic representation - sym_name = 'Symbol\'s Name + Addend' - if sym_name not in rel and 'Symbol\'s Name' in rel: - # Adapt to different versions of readelf - rel[sym_name] = rel['Symbol\'s Name'] + '+0' - if rel[sym_name]: - p = rel[sym_name].split('+') - if len(p) == 1: - p = ['', p[0]] - rel[sym_name] = (p[0], int(p[1], 16)) - - if toc is None: - error(f"failed to analyze relocations in {f}") - - return rels - -def collect_sections(f): - """Collect section info from ELF.""" - - out, _ = run(['readelf', '-SW', f]) - - toc = None - sections = [] - for line in out.splitlines(): - line = line.strip() - if not line: - continue - line = re.sub(r'\[\s+', '[', line) - words = re.split(r' +', line) - if line.startswith('[Nr]'): # Header? - if toc is not None: - error("multiple headers in output of readelf") - toc = make_toc(words, {'Addr' : 'Address'}) - elif line.startswith('[') and toc is not None: - sec = parse_row(words, toc, ['Address', 'Off', 'Size']) - if 'A' in sec['Flg']: # Allocatable section? - sections.append(sec) - - if toc is None: - error(f"failed to analyze sections in {f}") - - return sections - -def read_unrelocated_data(input_name, syms, secs): - """Collect unrelocated data from ELF.""" - data = {} - with open(input_name, 'rb') as f: - def is_symbol_in_section(sym, sec): - sec_end = sec['Address'] + sec['Size'] - is_start_in_section = sec['Address'] <= sym['Value'] < sec_end - is_end_in_section = sym['Value'] + sym['Size'] <= sec_end - return is_start_in_section and is_end_in_section - for name, s in sorted(syms.items(), key=lambda s: s[1]['Value']): - # TODO: binary search (bisect) - sec = [sec for sec in secs if is_symbol_in_section(s, sec)] - if len(sec) != 1: - error(f"failed to locate section for interval [{s['Value']:x}, {s['Value'] + s['Size']:x})") - sec = sec[0] - f.seek(sec['Off']) - data[name] = f.read(s['Size']) - return data - -def collect_relocated_data(syms, bites, rels, ptr_size, reloc_types): - """Identify relocations for each symbol""" - data = {} - for name, s in sorted(syms.items()): - b = bites.get(name) - assert b is not None - if s['Demangled Name'].startswith('typeinfo name'): - data[name] = [('byte', int(x)) for x in b] - continue - data[name] = [] - for i in range(0, len(b), ptr_size): - val = int.from_bytes(b[i*ptr_size:(i + 1)*ptr_size], byteorder='little') - data[name].append(('offset', val)) - start = s['Value'] - finish = start + s['Size'] - # TODO: binary search (bisect) - for rel in rels: - if rel['Type'] in reloc_types and start <= rel['Offset'] < finish: - i = (rel['Offset'] - start) // ptr_size - assert i < len(data[name]) - data[name][i] = 'reloc', rel - return data - -def generate_vtables(cls_tables, cls_syms, cls_data): - """Generate code for vtables""" - c_types = { - 'reloc' : 'const void *', - 'byte' : 'unsigned char', - 'offset' : 'size_t' - } - - ss = [] - ss.append('''\ -#ifdef __cplusplus -extern "C" { -#endif - -''') - - # Print externs - - printed = set() - for name, data in sorted(cls_data.items()): - for typ, val in data: - if typ != 'reloc': - continue - sym_name, addend = val['Symbol\'s Name + Addend'] - sym_name = re.sub(r'@.*', '', sym_name) # Can we pin version in C? - if sym_name not in cls_syms and sym_name not in printed: - ss.append(f'''\ -extern const char {sym_name}[]; - -''') - - # Collect variable infos - - code_info = {} - - for name, s in sorted(cls_syms.items()): - data = cls_data[name] - if s['Demangled Name'].startswith('typeinfo name'): - declarator = 'const unsigned char %s[]' - else: - field_types = (f'{c_types[typ]} field_{i};' for i, (typ, _) in enumerate(data)) - declarator = 'const struct { %s } %%s' % ' '.join(field_types) # pylint: disable=C0209 # consider-using-f-string - vals = [] - for typ, val in data: - if typ != 'reloc': - vals.append(str(val) + 'UL') - else: - sym_name, addend = val['Symbol\'s Name + Addend'] - sym_name = re.sub(r'@.*', '', sym_name) # Can we pin version in C? - vals.append(f'(const char *)&{sym_name} + {addend}') - code_info[name] = (declarator, '{ %s }' % ', '.join(vals)) # pylint: disable= C0209 # consider-using-f-string - - # Print declarations - - for name, (decl, _) in sorted(code_info.items()): - type_name = name + '_type' - type_decl = decl % type_name - ss.append(f'''\ -typedef {type_decl}; -extern __attribute__((weak)) {type_name} {name}; -''') - - # Print definitions - - for name, (_, init) in sorted(code_info.items()): - type_name = name + '_type' - ss.append(f'''\ -const {type_name} {name} = {init}; -''') - - ss.append('''\ -#ifdef __cplusplus -} // extern "C" -#endif -''') - - return ''.join(ss) - -def read_soname(f): - """Read ELF's SONAME.""" - - out, _ = run(['readelf', '-d', f]) - - for line in out.splitlines(): - line = line.strip() - if not line: - continue - # 0x000000000000000e (SONAME) Library soname: [libndp.so.0] - soname_match = re.search(r'\(SONAME\).*\[(.+)\]', line) - if soname_match is not None: - return soname_match[1] - - return None - -def read_library_name(filename): - """Read library name from .def file.""" - - with open(filename, 'r') as f: - for line in f.readlines(): - line = line.strip() - m = re.match(r'^(?:LIBRARY|NAME)\s+([A-Za-z0-9_.\-]+)$', line) - if m is not None: - return m[1] - - return None - -def main(): - """Driver function""" - parser = argparse.ArgumentParser(description="Generate wrappers for shared library functions.", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=f"""\ -Examples: - $ python3 {me} /usr/lib/x86_64-linux-gnu/libaccountsservice.so.0 - Generating libaccountsservice.so.0.tramp.S... - Generating libaccountsservice.so.0.init.c... -""") - - parser.add_argument('library', - metavar='LIB', - help="Library to be wrapped (or .def file with list of functions).") - parser.add_argument('--verbose', '-v', - help="Print diagnostic info", - action='count', - default=0) - parser.add_argument('--dlopen', - help="Emit dlopen call (default)", - dest='dlopen', action='store_true', default=True) - parser.add_argument('--no-dlopen', - help="Do not emit dlopen call (user must load/unload library himself)", - dest='dlopen', action='store_false') - parser.add_argument('--dlopen-callback', - help="Call user-provided custom callback to load library instead of dlopen", - default='') - parser.add_argument('--dlsym-callback', - help="Call user-provided custom callback to resolve a symbol, " - "instead of dlsym", - default='') - parser.add_argument('--library-load-name', - help="Use custom name for dlopened library (default is SONAME)") - parser.add_argument('--lazy-load', - help="Load library on first call to any of it's functions (default)", - dest='lazy_load', action='store_true', default=True) - parser.add_argument('--no-lazy-load', - help="Load library at program start", - dest='lazy_load', action='store_false') - parser.add_argument('--thread-safe', - help="Ensure thread-safety (default)", - dest='thread_safe', action='store_true', default=True) - parser.add_argument('--no-thread-safe', - help="Do not ensure thread-safety", - dest='thread_safe', action='store_false') - parser.add_argument('--vtables', - help="Intercept virtual tables (EXPERIMENTAL)", - dest='vtables', action='store_true', default=False) - parser.add_argument('--no-vtables', - help="Do not intercept virtual tables (default)", - dest='vtables', action='store_false') - parser.add_argument('--no-weak-symbols', - help="Don't bind weak symbols", dest='no_weak_symbols', - action='store_true', default=False) - parser.add_argument('--target', - help="Target platform triple e.g. x86_64-unknown-linux-gnu or arm-none-eabi " - "(atm x86_64, i[0-9]86, arm/armhf/armeabi, aarch64/armv8, " - "mips/mipsel, mips64/mip64el, e2k, powerpc64/powerpc64le, " - "riscv64 are supported)", - default=os.uname()[-1]) - parser.add_argument('--symbol-list', - help="Path to file with symbols that should be present in wrapper " - "(all by default)") - parser.add_argument('--symbol-prefix', - metavar='PFX', - help="Prefix wrapper symbols with PFX", - default='') - parser.add_argument('-q', '--quiet', - help="Do not print progress info", - action='store_true') - parser.add_argument('--outdir', '-o', - help="Path to create wrapper at", - default='./') - - args = parser.parse_args() - - input_name = args.library - verbose = args.verbose - dlopen_callback = args.dlopen_callback - dlsym_callback = args.dlsym_callback - dlopen = args.dlopen - lazy_load = args.lazy_load - thread_safe = args.thread_safe - if args.target.startswith('arm'): - target = 'arm' # Handle armhf-..., armel-... - elif re.match(r'^i[0-9]86', args.target): - target = 'i386' - elif args.target.startswith('amd64'): - target = 'x86_64' - elif args.target.startswith('mips64'): - target = 'mips64' # Handle mips64-..., mips64el-..., mips64le-... - elif args.target.startswith('mips'): - target = 'mips' # Handle mips-..., mipsel-..., mipsle-... - elif args.target.startswith('ppc64le'): - target = 'powerpc64le' - elif args.target.startswith('ppc64'): - target = 'powerpc64' - elif args.target.startswith('rv64'): - target = 'riscv64' - else: - target = args.target.split('-')[0] - quiet = args.quiet - outdir = args.outdir - - if not os.path.exists(outdir): - os.makedirs(outdir) - - if args.symbol_list is None: - funs = None - else: - with open(args.symbol_list, 'r') as f: - funs = [] - for line in re.split(r'\r?\n', f.read()): - line = re.sub(r'#.*', '', line) - line = line.strip() - if line: - funs.append(line) - - binary = is_binary_file(input_name) - stem = os.path.basename(input_name) - if not binary: - stem = re.sub(r'\.def$', '', stem) - - if args.library_load_name is not None: - load_name = args.library_load_name - elif binary: - load_name = read_soname(input_name) - if load_name is None: - load_name = stem - else: - load_name = read_library_name(input_name) - if load_name is None: - load_name = stem - - # Collect target info - - target_dir = os.path.join(root, 'arch', target) - - if not os.path.exists(target_dir): - error(f"unknown architecture '{target}'") - - cfg = configparser.ConfigParser(inline_comment_prefixes=';') - cfg.read(target_dir + '/config.ini') - - ptr_size = int(cfg['Arch']['PointerSize']) - symbol_reloc_types = set(re.split(r'\s*,\s*', cfg['Arch']['SymbolReloc'])) - - def is_exported(s): - conditions = [ - s['Bind'] != 'LOCAL', - s['Type'] != 'NOTYPE', - s['Ndx'] != 'UND', - s['Name'] not in ['', '_init', '_fini']] - if args.no_weak_symbols: - conditions.append(s['Bind'] != 'WEAK') - return all(conditions) - - if binary: - syms = collect_syms(input_name) - else: - syms = collect_def_exports(input_name) - - # Also collected demangled names - if syms: - out, _ = run(['c++filt'], '\n'.join((sym['Name'] for sym in syms))) - out = out.rstrip("\n") # Some c++filts append newlines at the end - for i, name in enumerate(out.split("\n")): - syms[i]['Demangled Name'] = name - - syms = list(filter(is_exported, syms)) - - def is_data_symbol(s): - return (s['Type'] == 'OBJECT' - # Allow vtables if --vtables is on - and not (' for ' in s['Demangled Name'] and args.vtables)) - - exported_data = [s['Name'] for s in syms if is_data_symbol(s)] - if exported_data: - # TODO: we can generate wrappers for const data without relocations (or only code relocations) - warn(f"library '{input_name}' contains data symbols which won't be intercepted: " - + ', '.join(exported_data)) - - # Collect functions - # TODO: warn if user-specified functions are missing - - orig_funs = filter(lambda s: s['Type'] == 'FUNC', syms) - - all_funs = set() - warn_versioned = False - for s in orig_funs: - if not s['Default']: - # TODO: support versions - if not warn_versioned: - warn(f"library {input_name} contains versioned symbols which are NYI") - warn_versioned = True - if verbose: - print(f"Skipping versioned symbol {s['Name']}") - continue - all_funs.add(s['Name']) - - if funs is None: - funs = sorted(list(all_funs)) - if not funs and not quiet: - warn(f"no public functions were found in {input_name}") - else: - missing_funs = [name for name in funs if name not in all_funs] - if missing_funs: - warn("some user-specified functions are not present in library: " + ', '.join(missing_funs)) - funs = [name for name in funs if name in all_funs] - - if verbose: - print("Exported functions:") - for i, fun in enumerate(funs): - print(f" {i}: {fun}") - - # Collect vtables - - if args.vtables: - if not binary: - error("vtables not supported for .def files") - - cls_tables = {} - cls_syms = {} - - for s in syms: - m = re.match(r'^(vtable|typeinfo|typeinfo name) for (.*)', s['Demangled Name']) - if m is not None and is_exported(s): - typ, cls = m.groups() - name = s['Name'] - cls_tables.setdefault(cls, {})[typ] = name - cls_syms[name] = s - - if verbose: - print("Exported classes:") - for cls, _ in sorted(cls_tables.items()): - print(f" {cls}") - - secs = collect_sections(input_name) - if verbose: - print("Sections:") - for sec in secs: - print(f" {sec['Name']}: [{sec['Address']:x}, {sec['Address'] + sec['Size']:x}), " - f"at {sec['Off']:x}") - - bites = read_unrelocated_data(input_name, cls_syms, secs) - - rels = collect_relocs(input_name) - if verbose: - print("Relocs:") - for rel in rels: - sym_add = rel['Symbol\'s Name + Addend'] - print(f" {rel['Offset']}: {sym_add}") - - cls_data = collect_relocated_data(cls_syms, bites, rels, ptr_size, symbol_reloc_types) - if verbose: - print("Class data:") - for name, data in sorted(cls_data.items()): - demangled_name = cls_syms[name]['Demangled Name'] - print(f" {name} ({demangled_name}):") +import lief + +ME: str = "implib-gen" + + +def set_me_from_argv0(argv0: str) -> None: + global ME + ME = os.path.basename(argv0) + + +def warn(msg: str) -> None: + sys.stderr.write(f"{ME}: warning: {msg}\n") + + +def error(msg: str) -> None: + sys.stderr.write(f"{ME}: error: {msg}\n") + sys.exit(1) + + +def die(msg: str) -> None: + sys.stderr.write(f"{ME}: error: {msg}\n") + sys.exit(1) + + +def info_printer(quiet: bool) -> Callable[[str], None]: + return lambda msg: None if quiet else print(msg) + + +MAGIC_ELF: bytes = b"\x7fELF" +MAGIC_MACHO: frozenset[bytes] = frozenset({ + b"\xfe\xed\xfa\xce", b"\xce\xfa\xed\xfe", b"\xfe\xed\xfa\xcf", b"\xcf\xfa\xed\xfe", + b"\xca\xfe\xba\xbe", b"\xbe\xba\xfe\xca", b"\xca\xfe\xba\xbf", b"\xbf\xba\xfe\xca", +}) + + +@dataclass(slots=True) +class Symbol: + name: str + bind: str + typ: str + ndx: str + value: int = 0 + size: int = 0 + default: bool = True + version: str | None = None + demangled: str | None = None + + # Pre-calculated score to avoid tuple allocation on every comparison access + _score: tuple[bool, bool, bool, bool, bool] = field(init=False, repr=False) + + def __post_init__(self) -> None: + self._score = ( + self.ndx != "UND", + self.bind != "LOCAL", + self.typ != "NOTYPE", + self.size > 0, + self.value != 0 + ) + + @property + def score(self) -> tuple[bool, bool, bool, bool, bool]: + return self._score + + +@dataclass(slots=True) +class SectionInfo: + name: str + address: int + offset: int + size: int + flags: str + + +@dataclass(slots=True) +class RelocationInfo: + offset: int + info: int + typ: str + symbol_addend: tuple[str, int] + + +class BackendError(RuntimeError): pass + + +class BinaryBackend(ABC): + format_name: str + path: str + arch: str | None + magic: bytes + _bin: Any + _loaded: bool + _next_addr_map: dict[int, int] | None + _func_sizes: dict[int, int] | None + + def __init__(self, path: str): + self.path = path + self.arch = None + self._bin = None + self._loaded = False + self._next_addr_map = None + self._func_sizes = None + + try: + with open(path, "rb") as f: + self.magic = f.read(4) + except OSError: + self.magic = b"" + + def set_arch(self, arch: str) -> None: + self.arch = arch + + @property + def is_def(self) -> bool: + if self.path.lower().endswith(".def"): + return True + try: + with open(self.path, "r", errors='ignore') as f: + return any(f.readline().strip().upper() == "EXPORTS" for _ in range(10)) + except OSError: + return False + + def _collect_def_symbols(self) -> list[Symbol]: + out: list[Symbol] = [] + try: + with open(self.path, "r") as f: + exports_found = False + for line in f: + clean_line = line.split(';')[0].strip() + if not clean_line: + continue + if clean_line.upper() == "EXPORTS": + exports_found = True + elif exports_found: + if m := re.match(r"^([A-Za-z0-9_]+)", clean_line): + out.append(Symbol(m.group(1), "GLOBAL", "FUNC", "1", demangled=m.group(1))) + except OSError as e: + error(f"failed to parse .def file '{self.path}': {e}") + return out + + def _read_def_library_name(self) -> str | None: + try: + with open(self.path, "r") as f: + for line in f: + if m := re.match(r"^(?:LIBRARY|NAME)\s+([A-Za-z0-9_.\-]+)$", line.strip(), re.I): + return m.group(1) + except OSError: + pass + return os.path.splitext(os.path.basename(self.path))[0] + ".so" + + @property + def binary(self) -> Any: + if not self._loaded: + self._loaded = True + if self.is_def: + return None + try: + parsed = lief.parse(self.path) + if parsed is None: + raise BackendError(f"LIEF failed to parse '{self.path}'") + + if isinstance(parsed, lief.MachO.FatBinary): + cpu = lief.MachO.Header.CPU_TYPE.ARM64 + self._bin = parsed.take(cpu) + if self._bin is None: + raise BackendError(f"Mach-O FatBinary '{self.path}' does not contain an ARM64 slice.") + else: + self._bin = parsed + except Exception as e: + error(str(e) if isinstance(e, BackendError) else f"LIEF failed to parse '{self.path}': {e}") + return self._bin + + @abstractmethod + def matches(self) -> bool: + ... + + @abstractmethod + def collect_symbols(self) -> list[Symbol]: + ... + + def read_data(self, address: int, size: int) -> bytes: + try: + return bytes(self.binary.get_content_from_virtual_address(address, size)) if self.binary else b"" + except Exception: + return b"" + + def default_load_name(self) -> str: + return self._read_def_library_name() or "" + + def collect_sections(self) -> list[SectionInfo]: + return [] + + def collect_relocations(self) -> list[RelocationInfo]: + return [] + + def supports_vtables(self) -> bool: + return False + + def byteorder(self) -> str: + return "little" + + def _get_exact_size(self, val: int, original_size: int = 0) -> int: + if original_size > 0: + return original_size + if not val or not self.binary: + return 0 + + if self._func_sizes is None: + # Optimize list duplication out by using islice + all_addrs = sorted({sym.value for sym in self.binary.symbols if sym.value != 0}) + self._next_addr_map = dict(zip(all_addrs, itertools.islice(all_addrs, 1, None))) + self._func_sizes = {f.address: f.size for f in self.binary.functions if f.size > 0} + + if val in self._func_sizes: + return self._func_sizes[val] + + assert self._next_addr_map is not None + limit = self._next_addr_map.get(val) + try: + if sec := self.binary.section_from_virtual_address(val): + sec_end = sec.virtual_address + sec.size + limit = min(limit, sec_end) if limit else sec_end + except Exception: + pass + + return max(0, limit - val) if limit else 0 + + +class ElfBackend(BinaryBackend): + format_name: str = "elf" + + @property + def binary(self) -> lief.ELF.Binary | None: + return super().binary + + @property + def default_platform(self) -> str: + return "linux" + + def matches(self) -> bool: + return self.is_def or self.magic == MAGIC_ELF + + def collect_symbols(self) -> list[Symbol]: + if not self.binary: + return self._collect_def_symbols() + + by_name: dict[str, Symbol] = {} + + for sym in itertools.chain(self.binary.dynamic_symbols, self.binary.symbols): + if not sym.name: + continue + + ndx = ( + "UND" if sym.imported or sym.shndx == 0 else + "ABS" if sym.shndx == 0xFFF1 else + "COM" if sym.shndx == 0xFFF2 else + str(sym.shndx) + ) + + ver_name, default = None, True + if sym.has_version and sym.symbol_version: + default = not (sym.symbol_version.value & 0x8000) + if sym.symbol_version.symbol_version_auxiliary: + ver_name = sym.symbol_version.symbol_version_auxiliary.name + + if sym.visibility == lief.ELF.Symbol.VISIBILITY.HIDDEN: + default = False + + s_obj = Symbol( + name=sym.name, bind=sym.binding.name, typ=sym.type.name, ndx=ndx, + value=sym.value, size=self._get_exact_size(sym.value, sym.size), + default=default, version=ver_name, demangled=sym.demangled_name or sym.name + ) + + existing = by_name.get(sym.name) + if not existing or s_obj.score > existing.score or (existing.version is None and s_obj.version is not None): + if existing and s_obj.version is None: + s_obj.version, s_obj.default = existing.version, existing.default + by_name[sym.name] = s_obj + elif s_obj.version is not None and existing.version is not None and s_obj.default and not existing.default: + by_name[sym.name] = s_obj + + if not by_name: + error(f"failed to analyze symbols in {self.path}") + return list(by_name.values()) + + def default_load_name(self) -> str: + if self.binary is None: + return self._read_def_library_name() or os.path.basename(self.path) + for entry in self.binary.dynamic_entries: + try: + if int(entry.tag) == 14: # DT_SONAME + return entry.name + except ValueError: + pass + return os.path.basename(self.path) + + def supports_vtables(self) -> bool: + return True + + def collect_sections(self) -> list[SectionInfo]: + if not self.binary: + return [] + return [ + SectionInfo(s.name, s.virtual_address, s.offset, s.size, "ALLOC") + for s in self.binary.sections if s.has(lief.ELF.Section.FLAGS.ALLOC) + ] + + def collect_relocations(self) -> list[RelocationInfo]: + if not self.binary: + return [] + + addr_syms = sorted([ + (s.value, s.value + max(1, s.size), s.name) + for s in self.binary.symbols if s.name and s.value != 0 and not s.name.startswith(".") + ], key=lambda t: t[0]) + starts = [t[0] for t in addr_syms] + + is_i386 = self.binary.header.machine_type == lief.ELF.ARCH.I386 + rels: list[RelocationInfo] = [] + + for rel in self.binary.relocations: + rel_type = f"R_{rel.type.name}".replace("R_X86_", "R_386_") if is_i386 else f"R_{rel.type.name}" + + try: + target_addr = rel.resolve() + except Exception: + target_addr = rel.addend + (rel.symbol.value if rel.has_symbol else 0) + + sym_name, addend = "", target_addr + if rel.has_symbol and rel.symbol.name and not rel.symbol.name.startswith("."): + sym_name, addend = rel.symbol.name, target_addr - rel.symbol.value + elif starts: + idx = bisect.bisect_right(starts, target_addr) - 1 + if idx >= 0: + start, end, name = addr_syms[idx] + if target_addr < end or target_addr - start <= 0x100000: + sym_name, addend = name, target_addr - start + + rels.append(RelocationInfo(rel.address, rel.info, rel_type, (sym_name, addend))) + return rels + + def byteorder(self) -> str: + return "little" if not self.binary or self.binary.header.identity_data == lief.ELF.Header.ELF_DATA.LSB else "big" + + +class MachOBackend(BinaryBackend): + format_name: str = "macho" + + @property + def binary(self) -> lief.MachO.Binary | None: + return super().binary + + def matches(self) -> bool: + return self.magic in MAGIC_MACHO + + def default_load_name(self) -> str: + if self.binary: + try: + if cmd := self.binary.get(lief.MachO.LoadCommand.TYPE.ID_DYLIB): + return os.path.basename(cmd.name) + except Exception: + pass + return os.path.basename(self.path) + + def collect_symbols(self) -> list[Symbol]: + if not self.binary: + return [] + + func_addrs = {f.address for f in self.binary.functions if f.size > 0} + by_name: dict[str, Symbol] = {} + + for sym in self.binary.symbols: + if not sym.name or sym.category == lief.MachO.Symbol.CATEGORY.NONE: + continue + + name = sym.name[1:] if sym.name.startswith("_") else sym.name + val, size = sym.value, sym.size + + try: + sec = self.binary.section_from_virtual_address(val) + except Exception: + sec = None + + typ, bind, ndx, default = "OBJECT", "LOCAL", "0", True + is_func = val in func_addrs + + if sec and not is_func: + flags = lief.MachO.Section.FLAGS + is_code = sec.has(flags.SOME_INSTRUCTIONS) or sec.has(flags.PURE_INSTRUCTIONS) + is_stub = sec.type == lief.MachO.Section.TYPE.SYMBOL_STUBS + + # Check execution segment safely depending on LIEF version + is_exec = False + if sec.has_segment and sec.segment: + try: + is_exec = bool(sec.segment.init_protection & lief.MachO.SegmentCommand.VM_PROTECTIONS.X.value) + except AttributeError: + is_exec = bool(sec.segment.init_protection & lief.MachO.SegmentCommand.VM_PROTECTIONS.X) + + if is_code or is_stub or is_exec: + is_func = True + + if is_func: + typ = "FUNC" + + if sym.category == lief.MachO.Symbol.CATEGORY.UNDEFINED: + ndx = "UND" + bind = "WEAK" if sym.has_binding_info and sym.binding_info.weak_import else "GLOBAL" + elif sym.category == lief.MachO.Symbol.CATEGORY.EXTERNAL: + bind = "GLOBAL" + if sym.has_export_info and sym.export_info: + flags = sym.export_info.flags_list + if lief.MachO.ExportInfo.FLAGS.WEAK_DEFINITION in flags: + bind = "WEAK" + if lief.MachO.ExportInfo.FLAGS.REEXPORT in flags: + typ = "FUNC" + else: + default = False + + s_obj = Symbol( + name, bind, typ, ndx, val, + self._get_exact_size(val, size), default, None, + demangled=sym.demangled_name or name + ) + + existing = by_name.get(name) + if not existing or s_obj.score > existing.score: + by_name[name] = s_obj + + if not by_name: + error(f"failed to analyze symbols in {self.path}") + return list(by_name.values()) + + def supports_vtables(self) -> bool: + return True + + def collect_sections(self) -> list[SectionInfo]: + if not self.binary: + return [] + return [ + SectionInfo( + s.name, s.virtual_address, s.offset, s.size, + "ALLOC" if s.has_segment and s.segment.name != "__PAGEZERO" else "" + ) for s in self.binary.sections + ] + + def collect_relocations(self) -> list[RelocationInfo]: + rels: list[RelocationInfo] = [] + if not self.binary: + return rels + + for rel in self.binary.relocations: + sym_name = rel.symbol.name if rel.has_symbol and rel.symbol else "" + if sym_name.startswith("_"): + sym_name = sym_name[1:] + rels.append(RelocationInfo(rel.address, 0, "SYMBOLIC" if sym_name else "RELATIVE", (sym_name, 0))) + + for b in self.binary.bindings: + sym_name = b.symbol.name if b.has_symbol and b.symbol else "" + if sym_name.startswith("_"): + sym_name = sym_name[1:] + rels.append(RelocationInfo(b.address, 0, "SYMBOLIC", (sym_name, b.addend))) + + return rels + + +@dataclass(frozen=True, slots=True) +class GenOptions: + verbose: int + quiet: bool + dlopen: bool + lazy_load: bool + thread_safe: bool + vtables: bool + no_weak_symbols: bool + symbol_prefix: str + dlopen_callback: str + dlsym_callback: str + ptr_size: int + symbol_reloc_types: set[str] + + +def _read_unrelocated_data(backend: BinaryBackend, syms: dict[str, Symbol]) -> dict[str, bytes]: + return {name: backend.read_data(s.value, s.size) for name, s in sorted(syms.items(), key=lambda it: it[1].value)} + + +def _collect_relocated_data(all_syms: list[Symbol], syms: dict[str, Symbol], bites: dict[str, bytes], + rels: list[RelocationInfo], ptr_size: int, reloc_types: set[str], *, byteorder: str, + demangled: dict[str, str]) -> dict[str, list]: + data: dict[str, list] = {} + addr_to_sym = {s.value: s.name for s in all_syms if s.value != 0} + + for name, s in sorted(syms.items()): + b, dname = bites[name], demangled.get(name, "") + if dname.startswith("typeinfo name") or "typeinfo name for" in dname: + data[name] = [("byte", int(x)) for x in b] + continue + + entries: list[tuple[str, Any]] = [ + ("offset", int.from_bytes(b[i:i + ptr_size], byteorder=byteorder, signed=False)) + for i in range(0, len(b), ptr_size) + ] + + for rel in rels: + if rel.typ in reloc_types and s.value <= rel.offset < s.value + s.size: + i = (rel.offset - s.value) // ptr_size + if i < len(entries): + sym_name, addend = rel.symbol_addend + if not sym_name and entries[i][0] == "offset": + sym_name, addend = addr_to_sym.get(entries[i][1] & 0x0000FFFFFFFFFFFF, ""), 0 + if sym_name: + entries[i] = ("reloc", RelocationInfo(rel.offset, 0, rel.typ, (sym_name, addend))) + data[name] = entries + return data + + +def _generate_vtables(cls_syms: dict[str, Symbol], cls_data: dict) -> str: + c_types = {"reloc": "const void *", "byte": "unsigned char", "offset": "size_t"} + ss: list[str] = ["#ifdef __cplusplus\nextern \"C\" {\n#endif\n"] + printed: set[str] = set() + code_info: dict[str, tuple[str, str]] = {} + + for _, data in sorted(cls_data.items()): for typ, val in data: - print(" " + str(val if typ != 'reloc' else val['Symbol\'s Name + Addend'])) - - # Generate assembly code - - suffix = stem - lib_suffix = re.sub(r'[^a-zA-Z_0-9]+', '_', suffix) - - tramp_file = f'{suffix}.tramp.S' - with open(os.path.join(outdir, tramp_file), 'w') as f: - if not quiet: - print(f"Generating {tramp_file}...") - with open(target_dir + '/table.S.tpl', 'r') as t: - table_text = string.Template(t.read()).substitute( - lib_suffix=lib_suffix, - table_size=ptr_size*(len(funs) + 1)) - f.write(table_text) - - with open(target_dir + '/trampoline.S.tpl', 'r') as t: - tramp_tpl = string.Template(t.read()) - - for i, name in enumerate(funs): - tramp_text = tramp_tpl.substitute( - lib_suffix=lib_suffix, - sym=args.symbol_prefix + name, - offset=i*ptr_size, - number=i) - f.write(tramp_text) - - # Generate C code - - init_file = f'{suffix}.init.c' - with open(os.path.join(outdir, init_file), 'w') as f: - if not quiet: - print(f"Generating {init_file}...") - with open(os.path.join(root, 'arch/common/init.c.tpl'), 'r') as t: - if funs: - sym_names = ',\n '.join(f'"{name}"' for name in funs) + ',' - else: - sym_names = '' - init_text = string.Template(t.read()).substitute( - lib_suffix=lib_suffix, - load_name=load_name, - dlopen_callback=dlopen_callback, - dlsym_callback=dlsym_callback, - has_dlopen_callback=int(bool(dlopen_callback)), - has_dlsym_callback=int(bool(dlsym_callback)), - no_dlopen=int(not dlopen), - lazy_load=int(lazy_load), - thread_safe=int(thread_safe), - sym_names=sym_names) - f.write(init_text) - if args.vtables: - vtable_text = generate_vtables(cls_tables, cls_syms, cls_data) - f.write(vtable_text) - -if __name__ == '__main__': - main() + if typ == "reloc": + sym_name = re.sub(r"@.*", "", val.symbol_addend[0]) + if sym_name and sym_name not in cls_syms and sym_name not in printed: + printed.add(sym_name) + ss.append(f"extern const char {sym_name}[];\n") + + for name, s in sorted(cls_syms.items()): + data = cls_data[name] + if data and data[0][0] == "byte": + declarator = "const unsigned char %s[]" + else: + fields = " ".join(f"{c_types[typ]} field_{i};" for i, (typ, _) in enumerate(data)) + declarator = f"const struct {{ {fields} }} %s" + + vals = [] + for typ, val in data: + if typ != "reloc": + vals.append(f"{val}UL") + else: + sym_name = re.sub(r"@.*", "", val.symbol_addend[0]) + vals.append( + f"(const char *)&{sym_name} + {val.symbol_addend[1]}" if sym_name else f"{val.symbol_addend[1]}UL" + ) + + code_info[name] = (declarator, "{ %s }" % ", ".join(vals)) + + for name, (decl, _) in sorted(code_info.items()): + t_name = f"{name}_type" + ss.append(f"typedef {decl % t_name};\n") + ss.append(f"extern __attribute__((weak)) {t_name} {name};\n") + + for name, (_, init) in sorted(code_info.items()): + t_name = f"{name}_type" + ss.append(f"const {t_name} {name} = {init};\n") + + ss.append("#ifdef __cplusplus\n} // extern \"C\"\n#endif\n") + return "".join(ss) + + +class Generator: + backend: BinaryBackend + templates_dir: Path + common_dir: Path + info: Callable[[str], None] + + def __init__(self, backend: BinaryBackend, *, templates_dir: str, common_templates_dir: str, + info: Callable[[str], None]) -> None: + self.backend = backend + self.templates_dir = Path(templates_dir) + self.common_dir = Path(common_templates_dir) + self.info = info + + def run(self, *, input_path: str, outdir: str, stem: str, load_name: str, funs_allowlist: list[str] | None, + opts: GenOptions) -> None: + Path(outdir).mkdir(parents=True, exist_ok=True) + all_exported_symbols = self.backend.collect_symbols() + demangled = {s.name: s.demangled for s in all_exported_symbols if s.demangled} + + syms_list: list[Symbol] = [] + warned_versioned: bool = False + + for s in all_exported_symbols: + if s.bind == "LOCAL" or s.typ == "NOTYPE" or s.ndx == "UND" or s.name in ("", "_init", "_fini"): + continue + if opts.no_weak_symbols and s.bind == "WEAK": + continue + + if not s.default: + if s.typ == "FUNC": + if not warned_versioned: + warn(f"library {input_path} contains hidden/versioned symbols which are not yet implemented") + warned_versioned = True + if opts.verbose: + self.info(f"Skipping hidden/versioned symbol {s.name}") + continue + + syms_list.append(s) + + def is_vtable_name(name: str, dname: str) -> bool: + return "vtable for " in dname or "typeinfo " in dname or "typeinfo name for " in dname or name.lstrip( + '_').startswith(("ZTV", "ZTI", "ZTS")) + + exported_data = [ + s.name for s in syms_list + if (s.typ in ("OBJECT", "COMMON", "TLS") or s.ndx == "COM") + and (not opts.vtables or not is_vtable_name(s.name, demangled.get(s.name, ""))) + ] + + if exported_data: + warn(f"library '{input_path}' contains data symbols which won't be intercepted: {', '.join(exported_data)}") + + all_funs = {s.name for s in syms_list if s.typ == "FUNC"} + funs = sorted(all_funs) if funs_allowlist is None else [n for n in funs_allowlist if n in all_funs] + + if funs_allowlist is None: + if not funs and not opts.quiet: + warn(f"no public functions were found in {input_path}") + elif missing := [n for n in funs_allowlist if n not in all_funs]: + warn(f"some user-specified functions are not present in library: {', '.join(missing)}") + + if opts.verbose: + self.info("Exported functions:") + for i, fn in enumerate(funs): + self.info(f" {i}: {fn}") + + vtable_text = "" + if opts.vtables: + if not self.backend.supports_vtables(): + error("vtables not supported for this file format") + + cls_tables: dict[str, dict[str, str]] = {} + cls_syms: dict[str, Symbol] = {} + + for s in syms_list: + dname = demangled.get(s.name, "") + if m := re.match(r"^(vtable|typeinfo|typeinfo name) for (.*)", dname): + typ, cls_name = m.groups() + cls_tables.setdefault(cls_name, {})[typ] = s.name + cls_syms[s.name] = s + elif is_vtable_name(s.name, dname): + name_no_und = s.name.lstrip('_') + for prefix, typ in [("ZTV", "vtable"), ("ZTI", "typeinfo"), ("ZTS", "typeinfo name")]: + if name_no_und.startswith(prefix): + cls_name = name_no_und[len(prefix):] + cls_tables.setdefault(cls_name, {})[typ] = s.name + cls_syms[s.name] = s + break + + if cls_syms: + vtable_text = _generate_vtables( + cls_syms, + _collect_relocated_data( + all_exported_symbols, cls_syms, + _read_unrelocated_data(self.backend, cls_syms), + self.backend.collect_relocations(), + opts.ptr_size, opts.symbol_reloc_types, + byteorder=self.backend.byteorder(), + demangled=demangled + ) + ) + + lib_suffix, tramp_file, init_file = re.sub(r"[^a-zA-Z_0-9]+", "_", stem), f"{stem}.tramp.S", f"{stem}.init.c" + if not opts.quiet: + self.info(f"Generating {tramp_file}...") + + with open(os.path.join(outdir, tramp_file), "w") as f: + f.write(string.Template((self.templates_dir / "table.S.tpl").read_text()).substitute( + lib_suffix=lib_suffix, + table_size=opts.ptr_size * (len(funs) + 1) + )) + tramp_tpl = string.Template((self.templates_dir / "trampoline.S.tpl").read_text()) + for i, name in enumerate(funs): + f.write(tramp_tpl.substitute( + lib_suffix=lib_suffix, sym=opts.symbol_prefix + name, + offset=i * opts.ptr_size, number=i + )) + + if not opts.quiet: + self.info(f"Generating {init_file}...") + + with open(os.path.join(outdir, init_file), "w") as f: + f.write(string.Template((self.common_dir / "init.c.tpl").read_text()).substitute( + lib_suffix=lib_suffix, load_name=load_name, dlopen_callback=opts.dlopen_callback, + dlsym_callback=opts.dlsym_callback, + has_dlopen_callback=int(bool(opts.dlopen_callback)), + has_dlsym_callback=int(bool(opts.dlsym_callback)), + no_dlopen=int(not opts.dlopen), lazy_load=int(opts.lazy_load), + thread_safe=int(opts.thread_safe), + sym_names=(",\n ".join(f'"{name}"' for name in funs) + ",") if funs else "" + ) + vtable_text) + + +def normalize_arch(raw: str) -> str: + if raw == "arm64": return "aarch64" + if raw.startswith("arm"): return "arm" + if re.match(r"^i[0-9]86", raw): return "i386" + if raw.startswith("amd64"): return "x86_64" + return raw.split("-")[0] + + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser() + p.add_argument("library") + p.add_argument("--platform", choices=["linux", "osx"], default=None) + p.add_argument("--target", default=os.uname().machine) + p.add_argument("--outdir", "-o", default="./") + p.add_argument("--symbol-list") + p.add_argument("--symbol-prefix", default="") + p.add_argument("--verbose", "-v", action="count", default=0) + p.add_argument("-q", "--quiet", action="store_true") + + for name, default in [("dlopen", True), ("lazy-load", True), ("thread-safe", True), ("vtables", False)]: + dest = name.replace("-", "_") + p.add_argument(f"--{name}", dest=dest, action="store_true", default=default) + p.add_argument(f"--no-{name}", dest=dest, action="store_false") + + p.add_argument("--no-weak-symbols", dest="no_weak_symbols", action="store_true", default=False) + p.add_argument("--dlopen-callback", default="") + p.add_argument("--dlsym-callback", default="") + p.add_argument("--library-load-name", default=None) + + args = p.parse_args(argv) + info = info_printer(args.quiet) + + platform = args.platform or ("osx" if sys.platform == "darwin" else "linux") + m_backend = MachOBackend(args.library) + e_backend = ElfBackend(args.library) + + if m_backend.matches(): + backend: BinaryBackend = m_backend + elif e_backend.matches(): + backend = e_backend + else: + backend = m_backend if args.platform == "osx" else e_backend + + platform_root = Path(__file__).resolve().parent / "arch" / platform + arch = normalize_arch(args.target) + backend.set_arch(arch) + + cfg_path = platform_root / arch / "config.ini" + if not cfg_path.exists(): + die(f"unknown architecture '{arch}' for platform '{platform_root.name}'") + + cfg = configparser.ConfigParser(inline_comment_prefixes=";") + cfg.read(cfg_path) + + stem = Path(args.library).name + if stem.lower().endswith(".def"): + stem = stem[:-4] + + opts = GenOptions( + args.verbose, args.quiet, args.dlopen, args.lazy_load, args.thread_safe, args.vtables, args.no_weak_symbols, + args.symbol_prefix, args.dlopen_callback, args.dlsym_callback, + int(cfg["Arch"]["PointerSize"]), set(re.split(r"\s*,\s*", cfg["Arch"]["SymbolReloc"])) + ) + + funs_allowlist = None + if args.symbol_list: + with open(args.symbol_list, "r") as f: + funs_allowlist = [line for l in f if (line := re.sub(r"#.*", "", l).strip())] + + Generator( + backend, + templates_dir=str(platform_root / arch), + common_templates_dir=str(platform_root / "common"), + info=info + ).run( + input_path=args.library, outdir=args.outdir, stem=stem, + load_name=args.library_load_name or backend.default_load_name(), + funs_allowlist=funs_allowlist, opts=opts + ) + return 0 + + +if __name__ == "__main__": + set_me_from_argv0(sys.argv[0]) + exit_code = main() + sys.exit(exit_code) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..867fa3f --- /dev/null +++ b/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from implib-so!") + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d4c46b7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +lief~=0.17.6 \ No newline at end of file diff --git a/scripts/ld b/scripts/ld index f58a51a..eb403b3 100755 --- a/scripts/ld +++ b/scripts/ld @@ -153,12 +153,15 @@ Flags can be specified directly or via IMPLIBSO_LD_OPTIONS environment variable. sys.stderr.write(err) sys.exit(rc) - # Analyze ldd output to see which runtime libs were linked - # TODO: support BSD's ldd output format - rc, out, err = run(f'ldd {out_filename}') + # Analyze linked libraries + if sys.platform == 'darwin': + rc, out, err = run(f'otool -L {out_filename}') + else: + rc, out, err = run(f'ldd {out_filename}') + os.unlink(out_filename) # Remove output in case we fail later if rc != 0: - error(f"ldd failed: {err}") + error(f"dependency check failed: {err}") sys.stderr.write(err) class WrapperInfo: @@ -174,16 +177,22 @@ Flags can be specified directly or via IMPLIBSO_LD_OPTIONS environment variable. l = l.strip() if not l: continue - if re.search(r'linux-vdso|ld-linux', l): + if re.search(r'linux-vdso|ld-linux|/usr/lib/libSystem|/usr/lib/libc\+\+', l): if v > 0: note(f"skipping system library: {l}") continue - m = re.search(r'^lib(\S*)\.so(\.[0-9]+)? => (\S*)', l) + + # Linux: libfoo.so.1 => /path/to/libfoo.so.1 (0x...) + # Mac: /path/to/libfoo.dylib (compatibility...) + m = re.search(r'^(?:lib)?(\S*?)\.(?:so|dylib)(?:\.[0-9]+)?(?: => )?(\S*)', l) if m is None: - warn(f"failed to parse ldd output: {l}") + if not l.endswith(':'): # Skip first line of otool + warn(f"failed to parse dependency output: {l}") continue + name = m.group(1) - path = m.group(3) + path = m.group(2) or m.group(0).split(' ')[0] + if is_system_library(name): if v > 0: note(f"skipping system library: {l}") @@ -217,7 +226,7 @@ Flags can be specified directly or via IMPLIBSO_LD_OPTIONS environment variable. o += '.o' if v > 0: note(f"compiling wrapper for {f} in {o}") - rc, out, err = run(f'gcc -Wall -Wextra -O2 -c -o {o} {f}') + rc, out, err = run(f'cc -Wall -Wextra -O2 -c -o {o} {f}') if rc != 0: error(f"implib-gen failed: {err}") sys.stderr.write(err) diff --git a/scripts/travis.sh b/scripts/travis.sh index eefd5a2..e108812 100755 --- a/scripts/travis.sh +++ b/scripts/travis.sh @@ -19,36 +19,78 @@ cd $(dirname $0)/.. ARCH=${ARCH:-} export PYTHON="${PYTHON:-python3}" -tests/basic/run.sh $ARCH -tests/exceptions/run.sh $ARCH -tests/data-warnings/run.sh $ARCH -tests/vtables/run.sh $ARCH +LOG_DIR=$(mktemp -d) +trap 'rm -rf "$LOG_DIR"' EXIT + +PID_LIST="" +run_test() { + name=$1 + shift + "$@" > "$LOG_DIR/$name.log" 2>&1 & + + pid=$! + PID_LIST="$PID_LIST $pid" + eval "NAME_$pid=\"$name\"" +} + +run_test "basic" tests/basic/run.sh $ARCH +run_test "exceptions" tests/exceptions/run.sh $ARCH +run_test "data-warnings" tests/data-warnings/run.sh $ARCH +run_test "vtables" tests/vtables/run.sh $ARCH + if test -z "$ARCH" && ! echo "${CC:-}" | grep -q musl-gcc; then - # TODO: enable for other targets - tests/ld/run.sh + run_test "ld" tests/ld/run.sh fi + if ! echo "$ARCH" | grep -q 'i[0-9]86'; then - # TODO: symtab on x86 seems to be corrupted - tests/multilib/run.sh $ARCH + run_test "multilib" tests/multilib/run.sh $ARCH fi -tests/hidden/run.sh $ARCH -tests/verbose/run.sh $ARCH -tests/no_dlopen/run.sh $ARCH -if ! echo "${CC:-}" | grep -q musl-gcc; then # Musl does not implement dlclose - tests/multiple-dlopens/run.sh $ARCH - tests/multiple-dlopens-2/run.sh $ARCH - tests/multiple-dlopens-3/run.sh $ARCH + +run_test "hidden" tests/hidden/run.sh $ARCH +run_test "verbose" tests/verbose/run.sh $ARCH +run_test "no_dlopen" tests/no_dlopen/run.sh $ARCH + +if ! echo "${CC:-}" | grep -q musl-gcc; then + run_test "multiple-dlopens" tests/multiple-dlopens/run.sh $ARCH + run_test "multiple-dlopens-2" tests/multiple-dlopens-2/run.sh $ARCH + run_test "multiple-dlopens-3" tests/multiple-dlopens-3/run.sh $ARCH fi + if ! echo "$ARCH" | grep -q powerpc; then - tests/many-functions/run.sh $ARCH + run_test "many-functions" tests/many-functions/run.sh $ARCH fi -tests/stack-args/run.sh $ARCH + +run_test "stack-args" tests/stack-args/run.sh $ARCH + if ! echo "$ARCH" | grep -q 'powerpc\|mips\|riscv'; then - # TODO: support vector types for remaining platforms - tests/vector-args/run.sh $ARCH + run_test "vector-args" tests/vector-args/run.sh $ARCH +fi + +run_test "thread" tests/thread/run.sh $ARCH +run_test "thread-2" tests/thread-2/run.sh $ARCH +run_test "def" tests/def/run.sh $ARCH + + +FAIL_COUNT=0 + +for pid in $PID_LIST; do + eval "name=\$NAME_$pid" + + if wait "$pid"; then + FAIL_COUNT=$((FAIL_COUNT)) + else + FAIL_COUNT=$((FAIL_COUNT + 1)) + fi + + if [ -f "$LOG_DIR/$name.log" ]; then + cat "$LOG_DIR/$name.log" + fi +done + +if [ "$FAIL_COUNT" -gt 0 ]; then + echo "----------------------------------------" + echo "Testing failed! Total suites failed: $FAIL_COUNT" + exit 1 fi -tests/thread/run.sh $ARCH -tests/thread-2/run.sh $ARCH -tests/def/run.sh $ARCH -echo 'All tests passed' +echo 'All tests passed' \ No newline at end of file diff --git a/tests/basic/run.sh b/tests/basic/run.sh index 99c2376..90cc423 100755 --- a/tests/basic/run.sh +++ b/tests/basic/run.sh @@ -49,7 +49,7 @@ for ADD_CFLAGS in '-no-pie' '-fPIE'; do # Build app $CC $CFLAGS $ADD_CFLAGS main.c test.c libinterposed.so.tramp.S libinterposed.so.init.c $LIBS - LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log + LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log diff test.ref a.out.log done done @@ -68,7 +68,7 @@ for ADD_GFLAGS in '' '--no-lazy-load'; do # Build app $CC $CFLAGS $ADD_CFLAGS main.c shlib.so - LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log + LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log diff test.ref a.out.log done diff --git a/tests/benchmark/implib.prof b/tests/benchmark/implib.prof new file mode 100644 index 0000000..ca7be87 Binary files /dev/null and b/tests/benchmark/implib.prof differ diff --git a/tests/benchmark/run.sh b/tests/benchmark/run.sh index b1bbbda..a44b0dc 100755 --- a/tests/benchmark/run.sh +++ b/tests/benchmark/run.sh @@ -24,38 +24,33 @@ fi CFLAGS="-g -O2 $CFLAGS" N=10 -# Need sudo for nice... -RUN='nice -n -20 taskset 1' +RUN='sudo nice -n -20 taskset 1' $CC $CFLAGS -shared -fPIC interposed.c -o libinterposed.so ${PYTHON:-} ../../implib-gen.py -q --target $TARGET libinterposed.so -export LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} +export LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} # Baseline - $CC $CFLAGS -DBASELINE main.c echo "Baseline:" $RUN time ./a.out # Normal - -$CC $CFLAGS main.c -L. -linterposed $LIBS +$CC $CFLAGS main.c -L. -linterposed -Wl,-rpath,. $LIBS echo "Normal:" $RUN time ./a.out # Implib - -$CC $CFLAGS main.c libinterposed.so.tramp.S libinterposed.so.init.c $LIBS +$CC $CFLAGS main.c libinterposed.so.tramp.S libinterposed.so.init.c -Wl,-rpath,. $LIBS echo "Implib:" $RUN time ./a.out # Implib (IMPLIB_EXPORT_SHIMS) - -$CC $CFLAGS -DIMPLIB_EXPORT_SHIMS -rdynamic main.c libinterposed.so.tramp.S libinterposed.so.init.c $LIBS +$CC $CFLAGS -DIMPLIB_EXPORT_SHIMS -rdynamic main.c libinterposed.so.tramp.S libinterposed.so.init.c -Wl,-rpath,. $LIBS echo "Implib (IMPLIB_EXPORT_SHIMS):" -$RUN time ./a.out +$RUN time ./a.out \ No newline at end of file diff --git a/tests/common.sh b/tests/common.sh index 7e65f32..ff758de 100644 --- a/tests/common.sh +++ b/tests/common.sh @@ -20,6 +20,10 @@ if test $ARCH = $(uname -m); then TARGET=$ARCH PREFIX= INTERP= + if uname | grep -q Darwin; then + # On Mac, LD_LIBRARY_PATH is DYLD_LIBRARY_PATH + export DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} + fi else # Simulate # (see .github/workflows/ci.yml for list of needed packages) diff --git a/tests/data-warnings/run.sh b/tests/data-warnings/run.sh index 5824607..0bdd1a0 100755 --- a/tests/data-warnings/run.sh +++ b/tests/data-warnings/run.sh @@ -19,7 +19,7 @@ fi . ../common.sh -export LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} +export LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $CC $CFLAGS -shared -fPIC interposed.c ${PYTHON:-} ../../implib-gen.py -q --target $TARGET a.out 2>err.log diff --git a/tests/def/run.sh b/tests/def/run.sh index 2e931fb..1a7af5b 100755 --- a/tests/def/run.sh +++ b/tests/def/run.sh @@ -30,5 +30,5 @@ ${PYTHON:-} ../../implib-gen.py -q --target $TARGET interposed.def $CC $CFLAGS -fPIE main.c interposed.tramp.S interposed.init.c $LIBS -LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log +LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log diff test.ref a.out.log diff --git a/tests/exceptions/run.sh b/tests/exceptions/run.sh index 5998226..e793ebb 100755 --- a/tests/exceptions/run.sh +++ b/tests/exceptions/run.sh @@ -28,7 +28,7 @@ if uname | grep -q BSD; then exit 0 fi -export LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} +export LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $CXX $CFLAGS -shared -fPIC interposed.cpp -o libinterposed.so $CXX $CFLAGS main.cpp -L. -linterposed diff --git a/tests/hidden/run.sh b/tests/hidden/run.sh index e998772..1930e21 100755 --- a/tests/hidden/run.sh +++ b/tests/hidden/run.sh @@ -19,7 +19,7 @@ fi . ../common.sh -export LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} +export LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $CC $CFLAGS -shared -fPIC interposed.c -o libinterposed.so ${PYTHON:-} ../../implib-gen.py -q --target $TARGET libinterposed.so @@ -27,14 +27,26 @@ ${PYTHON:-} ../../implib-gen.py -q --target $TARGET libinterposed.so $CC $CFLAGS -shared -fPIC user.c libinterposed.so.tramp.S libinterposed.so.init.c -o libuser.so $CC $CFLAGS -shared -fPIC user.c libinterposed.so.tramp.S libinterposed.so.init.c -DIMPLIB_EXPORT_SHIMS -o libuser_export_shims.so -if test $(readelf -D -sW libuser_export_shims.so | grep foo | wc -l) -eq 0; then - echo "Shim symbol NOT exported by default" >&2 - exit 1 -fi - -if test $(readelf -D -sW libuser.so | grep foo | wc -l) -gt 0; then - echo "Hidden shim symbol exported" >&2 - exit 1 +if uname | grep -q Darwin; then + # On Mac, check for _foo in exported symbols (nm -g) + if ! nm -g libuser_export_shims.so | grep -q _foo; then + echo "Shim symbol NOT exported" >&2 + exit 1 + fi + if nm -g libuser.so | grep -q _foo; then + echo "Hidden shim symbol exported" >&2 + exit 1 + fi +else + # On Linux, use readelf + if test $(readelf -D -sW libuser_export_shims.so | grep foo | wc -l) -eq 0; then + echo "Shim symbol NOT exported by default" >&2 + exit 1 + fi + if test $(readelf -D -sW libuser.so | grep foo | wc -l) -gt 0; then + echo "Hidden shim symbol exported" >&2 + exit 1 + fi fi echo SUCCESS diff --git a/tests/ld/run.sh b/tests/ld/run.sh index 5e57394..b41b28d 100755 --- a/tests/ld/run.sh +++ b/tests/ld/run.sh @@ -19,7 +19,7 @@ fi . ../common.sh -export LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} +export LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $CC $CFLAGS -shared -fPIC interposed.c -o libinterposed.so $CC $CFLAGS main.c -L. -linterposed @@ -33,9 +33,16 @@ if $CC --version | grep -qE 'clang|^lcc'; then CFLAGS="$CFLAGS -B." fi PATH=.:../..:$PATH $CC $CFLAGS -Wno-deprecated main.c -L. -linterposed -if readelf -d a.out | grep -q libinterposed; then - echo "Linker wrapper failed to wrap library" - exit 1 +if uname | grep -q Darwin; then + if otool -L a.out | grep -q libinterposed; then + echo "Linker wrapper failed to wrap library" + exit 1 + fi +else + if readelf -d a.out | grep -q libinterposed; then + echo "Linker wrapper failed to wrap library" + exit 1 + fi fi $INTERP ./a.out 2>&1 | tee new.log diff --git a/tests/many-functions/run.sh b/tests/many-functions/run.sh index 30fec15..203b8d0 100755 --- a/tests/many-functions/run.sh +++ b/tests/many-functions/run.sh @@ -28,4 +28,4 @@ ${PYTHON:-} ../../implib-gen.py -q --target $TARGET libtest.so $CC $CFLAGS libtest.so.* main.c $LIBS -LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} $INTERP ./a.out +LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $INTERP ./a.out diff --git a/tests/multilib/run.sh b/tests/multilib/run.sh index bd5c623..2875373 100755 --- a/tests/multilib/run.sh +++ b/tests/multilib/run.sh @@ -20,7 +20,7 @@ fi . ../common.sh -export LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} +export LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} LIB_CFLAGS='-shared -fPIC -fvisibility=hidden' diff --git a/tests/multiple-dlopens-2/run.sh b/tests/multiple-dlopens-2/run.sh index c1cac84..e4620d2 100755 --- a/tests/multiple-dlopens-2/run.sh +++ b/tests/multiple-dlopens-2/run.sh @@ -28,7 +28,7 @@ ${PYTHON:-} ../../implib-gen.py -q --target $TARGET --dlopen-callback=my_load_li # Build app $CC $CFLAGS -fPIE main.c libinterposed.so.tramp.S libinterposed.so.init.c $LIBS -LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log +LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log diff test.ref a.out.log echo SUCCESS diff --git a/tests/multiple-dlopens-3/run.sh b/tests/multiple-dlopens-3/run.sh index cedfa34..ce412f5 100755 --- a/tests/multiple-dlopens-3/run.sh +++ b/tests/multiple-dlopens-3/run.sh @@ -35,7 +35,7 @@ ${PYTHON:-} ../../implib-gen.py -q --target $TARGET --no-dlopen libinterposed.so # Build app $CC $CFLAGS -fPIE main.c libinterposed.so.tramp.S libinterposed.so.init.c $LIBS -LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log +LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log diff test.ref a.out.log echo SUCCESS diff --git a/tests/multiple-dlopens/run.sh b/tests/multiple-dlopens/run.sh index cedfa34..ce412f5 100755 --- a/tests/multiple-dlopens/run.sh +++ b/tests/multiple-dlopens/run.sh @@ -35,7 +35,7 @@ ${PYTHON:-} ../../implib-gen.py -q --target $TARGET --no-dlopen libinterposed.so # Build app $CC $CFLAGS -fPIE main.c libinterposed.so.tramp.S libinterposed.so.init.c $LIBS -LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log +LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log diff test.ref a.out.log echo SUCCESS diff --git a/tests/no_dlopen/run.sh b/tests/no_dlopen/run.sh index e3f42ae..2c7cff8 100755 --- a/tests/no_dlopen/run.sh +++ b/tests/no_dlopen/run.sh @@ -33,6 +33,27 @@ else READELF=readelf fi +check_symbol() { + local FILE=$1 + local SYM=$2 + local EXPECT_PRESENT=$3 + if uname | grep -q Darwin; then + # On Mac + if [ "$EXPECT_PRESENT" = "1" ]; then + nm -g "$FILE" | grep -q "_$SYM" + else + ! nm -g "$FILE" | grep -q "_$SYM" + fi + else + # On Linux + if [ "$EXPECT_PRESENT" = "1" ]; then + $READELF -sW --dyn-syms "$FILE" | grep -q "GLOBAL.*$SYM" + else + ! $READELF -sW --dyn-syms "$FILE" | grep -q "GLOBAL.*$SYM" + fi + fi +} + # Build shlib to test against $CC $CFLAGS -shared -fPIC interposed.c -o libinterposed.so @@ -45,9 +66,9 @@ ${PYTHON:-} ../../implib-gen.py -q --target $TARGET --no-dlopen libinterposed.so # Build app $CC $CFLAGS main.c test.c libinterposed.so.tramp.S libinterposed.so.init.c $LIBS -! ($READELF -sW --dyn-syms a.out | grep -q GLOBAL.*foo) +check_symbol a.out foo 0 -LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log +LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log diff test.ref a.out.log ######################################### @@ -59,9 +80,9 @@ ${PYTHON:-} ../../implib-gen.py -q --target $TARGET --no-dlopen libinterposed.so # Build app $CC $CFLAGS -DIMPLIB_EXPORT_SHIMS main.c test.c libinterposed.so.tramp.S libinterposed.so.init.c $LIBS -$READELF -sW --dyn-syms a.out | grep -q GLOBAL.*foo +check_symbol a.out foo 1 -LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log +LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log diff test.ref a.out.log ########## @@ -73,12 +94,12 @@ ${PYTHON:-} ../../implib-gen.py -q --target $TARGET --no-dlopen libinterposed.so # Build shlib $CC $CFLAGS -shared -fPIC shlib.c test.c libinterposed.so.tramp.S libinterposed.so.init.c $LIBS -o shlib.so -! ($READELF -sW --dyn-syms shlib.so | grep -q GLOBAL.*foo) +check_symbol shlib.so foo 0 # Build app $CC $CFLAGS main.c shlib.so $LIBS -LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log +LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log diff test.ref a.out.log ######################### @@ -90,12 +111,12 @@ ${PYTHON:-} ../../implib-gen.py -q --target $TARGET --no-dlopen libinterposed.so # Build shlib $CC $CFLAGS -DIMPLIB_EXPORT_SHIMS -shared -fPIC shlib.c test.c libinterposed.so.tramp.S libinterposed.so.init.c $LIBS -o shlib.so -$READELF -sW --dyn-syms shlib.so | grep -q GLOBAL.*foo +check_symbol shlib.so foo 1 # Build app $CC $CFLAGS main.c shlib.so $LIBS -LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log +LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log diff test.ref a.out.log echo SUCCESS diff --git a/tests/stack-args/run.sh b/tests/stack-args/run.sh index 39e0bfc..c32f1d4 100755 --- a/tests/stack-args/run.sh +++ b/tests/stack-args/run.sh @@ -32,10 +32,10 @@ ${PYTHON:-} ../../implib-gen.py -q --target $TARGET libinterposed.so # Build app $CC $CFLAGS main.c libinterposed.so.tramp.S libinterposed.so.init.c $LIBS -LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} $INTERP ./a.out int > a.out.log +LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $INTERP ./a.out int > a.out.log diff test.ref a.out.log -LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} $INTERP ./a.out float > a.out.log +LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $INTERP ./a.out float > a.out.log diff test.ref a.out.log echo SUCCESS diff --git a/tests/thread-2/main.c b/tests/thread-2/main.c index b35cc8e..7ba27b4 100644 --- a/tests/thread-2/main.c +++ b/tests/thread-2/main.c @@ -15,6 +15,41 @@ #include #include +#ifdef __APPLE__ +#ifndef PTHREAD_BARRIER_SERIAL_THREAD +#define PTHREAD_BARRIER_SERIAL_THREAD 1 +#endif +typedef struct { + pthread_mutex_t mutex; + pthread_cond_t cond; + unsigned int count; + unsigned int tripCount; +} pthread_barrier_t; +static inline int pthread_barrier_init(pthread_barrier_t *barrier, const void *attr, unsigned int count) { + (void)attr; + if (count == 0) return -1; + if (pthread_mutex_init(&barrier->mutex, 0) < 0) return -1; + if (pthread_cond_init(&barrier->cond, 0) < 0) return -1; + barrier->tripCount = count; + barrier->count = 0; + return 0; +} +static inline int pthread_barrier_wait(pthread_barrier_t *barrier) { + pthread_mutex_lock(&barrier->mutex); + ++(barrier->count); + if (barrier->count >= barrier->tripCount) { + barrier->count = 0; + pthread_cond_broadcast(&barrier->cond); + pthread_mutex_unlock(&barrier->mutex); + return PTHREAD_BARRIER_SERIAL_THREAD; + } else { + pthread_cond_wait(&barrier->cond, &barrier->mutex); + pthread_mutex_unlock(&barrier->mutex); + return 0; + } +} +#endif + #include "interposed.h" #if defined __mips && __mips == 32 diff --git a/tests/thread-2/run.sh b/tests/thread-2/run.sh index f3f900c..1014039 100755 --- a/tests/thread-2/run.sh +++ b/tests/thread-2/run.sh @@ -33,7 +33,7 @@ ${PYTHON:-} ../../implib-gen.py -q --target $TARGET libinterposed.so $CC $CFLAGS -fPIE main.c libinterposed.so.tramp.S libinterposed.so.init.c $LIBS for i in $(seq 1 $N); do - LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log + LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log diff test.ref a.out.log done @@ -42,7 +42,7 @@ done $CC $CFLAGS -DIMPLIB_EXPORT_SHIMS -fPIE main.c libinterposed.so.tramp.S libinterposed.so.init.c $LIBS for i in $(seq 1 $N); do - LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log + LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log diff test.ref a.out.log done @@ -57,7 +57,7 @@ if test -n "$TSAN_AVAILABLE"; then $CC $CFLAGS -g -fsanitize=thread -fPIE main.c libinterposed.so.tramp.S libinterposed.so.init.c $LIBS for i in $(seq 1 $N); do - LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log + LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log diff test.ref a.out.log done fi diff --git a/tests/thread/main.c b/tests/thread/main.c index c91dac3..8dd687a 100644 --- a/tests/thread/main.c +++ b/tests/thread/main.c @@ -15,6 +15,41 @@ #include #include +#ifdef __APPLE__ +#ifndef PTHREAD_BARRIER_SERIAL_THREAD +#define PTHREAD_BARRIER_SERIAL_THREAD 1 +#endif +typedef struct { + pthread_mutex_t mutex; + pthread_cond_t cond; + unsigned int count; + unsigned int tripCount; +} pthread_barrier_t; +static inline int pthread_barrier_init(pthread_barrier_t *barrier, const void *attr, unsigned int count) { + (void)attr; + if (count == 0) return -1; + if (pthread_mutex_init(&barrier->mutex, 0) < 0) return -1; + if (pthread_cond_init(&barrier->cond, 0) < 0) return -1; + barrier->tripCount = count; + barrier->count = 0; + return 0; +} +static inline int pthread_barrier_wait(pthread_barrier_t *barrier) { + pthread_mutex_lock(&barrier->mutex); + ++(barrier->count); + if (barrier->count >= barrier->tripCount) { + barrier->count = 0; + pthread_cond_broadcast(&barrier->cond); + pthread_mutex_unlock(&barrier->mutex); + return PTHREAD_BARRIER_SERIAL_THREAD; + } else { + pthread_cond_wait(&barrier->cond, &barrier->mutex); + pthread_mutex_unlock(&barrier->mutex); + return 0; + } +} +#endif + #include "interposed.h" #if defined __mips && __mips == 32 diff --git a/tests/thread/run.sh b/tests/thread/run.sh index 092f279..a61278a 100755 --- a/tests/thread/run.sh +++ b/tests/thread/run.sh @@ -36,7 +36,7 @@ ${PYTHON:-} ../../implib-gen.py -q --target $TARGET libinterposed.so $CC $CFLAGS -fPIE main.c libinterposed.so.tramp.S libinterposed.so.init.c $LIBS for i in $(seq 1 $N); do - LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log + LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log diff test.ref a.out.log done @@ -45,7 +45,7 @@ done $CC $CFLAGS -DIMPLIB_EXPORT_SHIMS -fPIE main.c libinterposed.so.tramp.S libinterposed.so.init.c $LIBS for i in $(seq 1 $N); do - LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log + LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log diff test.ref a.out.log done @@ -60,7 +60,7 @@ if test -n "$TSAN_AVAILABLE"; then $CC $CFLAGS -g -fsanitize=thread -fPIE main.c libinterposed.so.tramp.S libinterposed.so.init.c $LIBS for i in $(seq 1 $N); do - LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log + LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log diff test.ref a.out.log done fi diff --git a/tests/vector-args/run.sh b/tests/vector-args/run.sh index d6ef4eb..8821297 100755 --- a/tests/vector-args/run.sh +++ b/tests/vector-args/run.sh @@ -32,7 +32,7 @@ ${PYTHON:-} ../../implib-gen.py -q --target $TARGET libinterposed.so # Build app $CC $CFLAGS main.c libinterposed.so.tramp.S libinterposed.so.init.c $LIBS -LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log +LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $INTERP ./a.out > a.out.log diff test.ref a.out.log echo SUCCESS diff --git a/tests/verbose/run.sh b/tests/verbose/run.sh index ff41b02..2d15036 100755 --- a/tests/verbose/run.sh +++ b/tests/verbose/run.sh @@ -19,7 +19,7 @@ fi . ../common.sh -export LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} +export LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $CXX $CFLAGS -shared -frtti -fPIC interposed.cpp -o libinterposed.so ${PYTHON:-} ../../implib-gen.py -vvv -q --target $TARGET --vtables libinterposed.so >/dev/null 2>&1 diff --git a/tests/vtables/run.sh b/tests/vtables/run.sh index dd9fc70..3e80208 100755 --- a/tests/vtables/run.sh +++ b/tests/vtables/run.sh @@ -19,7 +19,7 @@ fi . ../common.sh -export LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} +export LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH:-} DYLD_LIBRARY_PATH=.:${DYLD_LIBRARY_PATH:-} $CXX $CFLAGS -shared -fPIC interposed.cpp -o libinterposed.so $CXX $CFLAGS main.cpp -L. -linterposed