From ee280c391a01a34b76e0da7b2975bc9b74a7b348 Mon Sep 17 00:00:00 2001 From: Jing Sima Date: Fri, 17 Apr 2026 15:49:50 +0800 Subject: [PATCH 1/2] Add macOS Apple Silicon (arm64) support macOS (especially Apple Silicon) enforces W^X on the __TEXT segment at the page-table level and caps mprotect by the segment's maxprot. Two issues previously prevented cpp-stub from patching its own text segment on an M-chip Mac: 1. mprotect cannot raise __TEXT to PROT_WRITE|PROT_EXEC. 2. Even with a writable alias obtained via mach_vm_remap, a plain store instruction that happens to sit on the same physical page as the destination deadlocks the current thread. Fixes: * src/stub.h: on __APPLE__, obtain a writable alias of the target page(s) via mach_vm_remap(VM_INHERIT_SHARE) + mach_vm_protect(RW) and route every write through libSystem's memcpy (invoked through a volatile function pointer) so the store opcodes live on libSystem's pages rather than our own __TEXT. The aarch64 REPLACE_FAR macro is specialized for Apple to build the trampoline in a local buffer and memcpy it across. All non-Apple branches keep the original logic. * tool/macos_enable_stub.sh: post-link helper that walks the Mach-O load commands, patches __TEXT's maxprot to rwx, and ad-hoc re-signs the binary with codesign. Required once per test executable (see issue #49). * test/Makefile.darwin.clang: new Darwin Makefile that drives clang++ and runs macos_enable_stub.sh automatically. * .github/workflows/make-test-multi-platform.yml: enable macOS-13 (Intel) and macOS-latest (Apple Silicon) jobs using the new Makefile, replacing the commented-out stubs. * README / README_zh: document the new macOS support. Verified on macOS 26.4 arm64 (M-chip): test_function, test_class_member_function, test_variadic_function all pass, and repeated set/reset cycles behave correctly. Refs: #49 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../workflows/make-test-multi-platform.yml | 60 ++--- .gitignore | 1 + README.md | 6 +- README_zh.md | 6 +- src/stub.h | 210 +++++++++++++----- test/Makefile.darwin.clang | 32 +++ tool/macos_enable_stub.sh | 74 ++++++ 7 files changed, 292 insertions(+), 97 deletions(-) create mode 100644 test/Makefile.darwin.clang create mode 100755 tool/macos_enable_stub.sh diff --git a/.github/workflows/make-test-multi-platform.yml b/.github/workflows/make-test-multi-platform.yml index 483e777..1e1fce5 100644 --- a/.github/workflows/make-test-multi-platform.yml +++ b/.github/workflows/make-test-multi-platform.yml @@ -22,10 +22,10 @@ jobs: compiler: gcc - os: ubuntu-latest compiler: clang -# - os: macos-latest -# compiler: clang -# - os: macos-14 -# compiler: clang + - os: macos-13 + compiler: clang + - os: macos-latest + compiler: clang steps: - uses: actions/checkout@v4 @@ -70,49 +70,19 @@ jobs: echo "--- done RUNNING $f ---" done working-directory: ${{ github.workspace }}/test - - name: Build and run tests (Macos) - if: ${{ matrix.os == 'macos-latest' }} + - name: Build and run tests (macOS) + if: ${{ startsWith(matrix.os, 'macos') }} run: | - sysctl hw - make -f Makefile.linux64.clang - compiler=clang - for f in $(find . -type f -name "*.cpp"); do - if [[ -z "${f##*win.cpp}" ]]; then - continue - fi - - f=${f%.cpp} - - if [ ${compiler}="clang" ] && [[ $f == *virtual* ]]; then - continue - fi - + sysctl -n machdep.cpu.brand_string || true + uname -m + # Build + post-process (macos_enable_stub.sh is invoked automatically + # by the Makefile: it patches __TEXT maxprot to rwx and re-signs the + # binary ad-hoc so cpp-stub's Apple code path can create a writable + # alias of __TEXT at runtime). + make -f Makefile.darwin.clang + for f in $(find . -maxdepth 1 -type f -perm -u+x ! -name "Makefile*" ! -name "*.cpp" ! -name "*.md"); do echo "--- RUNNING $f ---" - printf '\x07' | dd of=${f} bs=1 seek=160 count=1 conv=notrunc - ${f} - echo "--- done RUNNING $f ---" - done - working-directory: ${{ github.workspace }}/test - - name: Build and run tests (Macos M1) - if: ${{ matrix.os == 'macos-14' }} - run: | - sysctl hw - make -f Makefile.linux64.clang - compiler=clang - for f in $(find . -type f -name "*.cpp"); do - if [[ -z "${f##*win.cpp}" ]]; then - continue - fi - - f=${f%.cpp} - - if [ ${compiler}="clang" ] && [[ $f == *virtual* ]]; then - continue - fi - - echo "--- RUNNING $f ---" - printf '\x07' | dd of=${f} bs=1 seek=160 count=1 conv=notrunc - ${f} + "$f" echo "--- done RUNNING $f ---" done working-directory: ${{ github.workspace }}/test diff --git a/.gitignore b/.gitignore index 97c3114..2b9c113 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .ccls-cache replit* +*.dSYM/ diff --git a/README.md b/README.md index 004d820..c9b9d55 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,11 @@ - Supported operating systems: * [x] Windows * [x] Linux - * [x] MacOS(x86-64, printf '\x07' | dd of=test_function bs=1 seek=160 count=1 conv=notrunc) + * [x] MacOS — both Intel (x86-64) and Apple Silicon (arm64). After building, run + `tool/macos_enable_stub.sh ` on each test executable to lift the + `__TEXT` segment's `maxprot` to `rwx` and ad-hoc re-sign (required by + macOS's W^X enforcement; see [issue #49](https://github.com/coolxv/cpp-stub/issues/49) + and [test/Makefile.darwin.clang](test/Makefile.darwin.clang) for an example). - Supported hardware platform: * [x] x86 * [x] x86-64 diff --git a/README_zh.md b/README_zh.md index 761a851..ddcff14 100644 --- a/README_zh.md +++ b/README_zh.md @@ -9,7 +9,11 @@ - 支持的操作系统 : * [x] Windows * [x] Linux - * [x] MacOS(x86-64, printf '\x07' | dd of=test_function bs=1 seek=160 count=1 conv=notrunc) + * [x] MacOS —— 同时支持 Intel (x86-64) 与 Apple Silicon (arm64)。编译完成后, + 对每个可执行文件执行 `tool/macos_enable_stub.sh `,把 `__TEXT` + 段的 `maxprot` 抬到 `rwx` 并做 ad-hoc 重新签名(macOS 的 W^X 强制保护 + 要求此步骤;参考 [issue #49](https://github.com/coolxv/cpp-stub/issues/49) + 以及 [test/Makefile.darwin.clang](test/Makefile.darwin.clang) 里的示例)。 - 支持的硬件平台 : * [x] x86 diff --git a/src/stub.h b/src/stub.h index 7c4e82c..9dd2b76 100644 --- a/src/stub.h +++ b/src/stub.h @@ -10,6 +10,11 @@ #include #include #endif +#if defined(__APPLE__) +#include +#include +#include +#endif //c #include #include @@ -49,6 +54,24 @@ #endif #endif +#if defined(__APPLE__) +// On Apple (especially Apple Silicon), a direct store instruction targeted +// at an alias of a __TEXT page deadlocks the current thread when the store +// opcode itself resides on the same physical page as the destination. +// To dodge this, every write to the patch site is routed through libSystem's +// memcpy via a volatile function pointer; that keeps the store opcodes on +// libSystem's pages rather than on our own __TEXT page. +static inline void __stub_apple_memcpy(void* dst, const void* src, std::size_t n) +{ + typedef void* (*__stub_memcpy_t)(void*, const void*, std::size_t); + volatile __stub_memcpy_t mc = (__stub_memcpy_t)&std::memcpy; + mc(dst, src, n); +} +#define STUB_WRITE_BYTES(dst, src, n) __stub_apple_memcpy((dst), (src), (n)) +#else +#define STUB_WRITE_BYTES(dst, src, n) std::memcpy((dst), (src), (n)) +#endif + #if defined(__aarch64__) || defined(_M_ARM64) #define CODESIZE 16U #define CODESIZE_MIN 16U @@ -56,11 +79,23 @@ // ldr x9, +8 // br x9 // addr +#if defined(__APPLE__) + #define REPLACE_FAR(t, fn, fn_stub)\ + do {\ + uint32_t __stub_buf[4];\ + __stub_buf[0] = 0x58000040 | 9;\ + __stub_buf[1] = 0xd61f0120 | (9 << 5);\ + *(long long *)(&__stub_buf[2]) = (long long)(fn_stub);\ + __stub_apple_memcpy((fn), __stub_buf, CODESIZE);\ + CACHEFLUSH((char *)(fn), CODESIZE);\ + } while(0) +#else #define REPLACE_FAR(t, fn, fn_stub)\ ((uint32_t*)fn)[0] = 0x58000040 | 9;\ ((uint32_t*)fn)[1] = 0xd61f0120 | (9 << 5);\ *(long long *)(fn + 8) = (long long )fn_stub;\ CACHEFLUSH((char *)fn, CODESIZE); +#endif #define REPLACE_NEAR(t, fn, fn_stub) REPLACE_FAR(t, fn, fn_stub) #elif defined(__arm__) || defined(_M_ARM) #define CODESIZE 8U @@ -392,32 +427,26 @@ class Stub for(iter=m_result.begin(); iter != m_result.end(); iter++) { pstub = iter->second; -#ifdef _WIN32 - DWORD lpflOldProtect; - if(0 != VirtualProtect(pageof(pstub->fn), m_pagesize * 2, PAGE_EXECUTE_READWRITE, &lpflOldProtect)) -#else - if (0 == mprotect(pageof(pstub->fn), m_pagesize * 2, PROT_READ | PROT_WRITE | PROT_EXEC)) -#endif + WriteSession ws; + if (!begin_write(pstub->fn, ws)) { + iter->second = NULL; + delete pstub; + continue; + } - if(pstub->far_jmp) - { - std::memcpy(pstub->fn, pstub->code_buf, CODESIZE_MAX); - } - else - { - std::memcpy(pstub->fn, pstub->code_buf, CODESIZE_MIN); - } - - CACHEFLUSH((char *)pstub->fn, CODESIZE); - -#ifdef _WIN32 - VirtualProtect(pageof(pstub->fn), m_pagesize * 2, PAGE_EXECUTE_READ, &lpflOldProtect); -#else - mprotect(pageof(pstub->fn), m_pagesize * 2, PROT_READ | PROT_EXEC); -#endif + if(pstub->far_jmp) + { + STUB_WRITE_BYTES(ws.write_addr, pstub->code_buf, CODESIZE_MAX); + } + else + { + STUB_WRITE_BYTES(ws.write_addr, pstub->code_buf, CODESIZE_MIN); } + CACHEFLUSH((char *)pstub->fn, CODESIZE); + end_write(pstub->fn, ws); + iter->second = NULL; delete pstub; } @@ -448,31 +477,33 @@ class Stub std::memcpy(pstub->code_buf, fn, CODESIZE_MIN); } -#ifdef _WIN32 - DWORD lpflOldProtect; - if(0 == VirtualProtect(pageof(pstub->fn), m_pagesize * 2, PAGE_EXECUTE_READWRITE, &lpflOldProtect)) -#else - if (-1 == mprotect(pageof(pstub->fn), m_pagesize * 2, PROT_READ | PROT_WRITE | PROT_EXEC)) -#endif + WriteSession ws; + if (!begin_write(pstub->fn, ws)) { + delete pstub; throw("stub set memory protect to w+r+x faild"); } + unsigned char * write_fn = ws.write_addr; if(pstub->far_jmp) { - REPLACE_FAR(this, fn, fn_stub); + REPLACE_FAR(this, write_fn, fn_stub); } else { - REPLACE_NEAR(this, fn, fn_stub); + REPLACE_NEAR(this, write_fn, fn_stub); } -#ifdef _WIN32 - if(0 == VirtualProtect(pageof(pstub->fn), m_pagesize * 2, PAGE_EXECUTE_READ, &lpflOldProtect)) -#else - if (-1 == mprotect(pageof(pstub->fn), m_pagesize * 2, PROT_READ | PROT_EXEC)) -#endif +#if defined(__APPLE__) + // Also flush i-cache at the original executable VA (the alias shares + // the same physical page, but invalidating the original VA explicitly + // is a safety belt for all cache topologies). + sys_icache_invalidate(pstub->fn, CODESIZE); +#endif + + if (!end_write(pstub->fn, ws)) { + delete pstub; throw("stub set memory protect to r+x failed"); } m_result.insert(std::pair(fn,pstub)); @@ -493,34 +524,28 @@ class Stub } struct func_stub *pstub; pstub = iter->second; - -#ifdef _WIN32 - DWORD lpflOldProtect; - if(0 == VirtualProtect(pageof(pstub->fn), m_pagesize * 2, PAGE_EXECUTE_READWRITE, &lpflOldProtect)) -#else - if (-1 == mprotect(pageof(pstub->fn), m_pagesize * 2, PROT_READ | PROT_WRITE | PROT_EXEC)) -#endif + + WriteSession ws; + if (!begin_write(pstub->fn, ws)) { throw("stub reset memory protect to w+r+x faild"); } if(pstub->far_jmp) { - std::memcpy(pstub->fn, pstub->code_buf, CODESIZE_MAX); + STUB_WRITE_BYTES(ws.write_addr, pstub->code_buf, CODESIZE_MAX); } else { - std::memcpy(pstub->fn, pstub->code_buf, CODESIZE_MIN); + STUB_WRITE_BYTES(ws.write_addr, pstub->code_buf, CODESIZE_MIN); } CACHEFLUSH((char *)pstub->fn, CODESIZE); +#if defined(__APPLE__) + sys_icache_invalidate(pstub->fn, CODESIZE); +#endif - -#ifdef _WIN32 - if(0 == VirtualProtect(pageof(pstub->fn), m_pagesize * 2, PAGE_EXECUTE_READ, &lpflOldProtect)) -#else - if (-1 == mprotect(pageof(pstub->fn), m_pagesize * 2, PROT_READ | PROT_EXEC)) -#endif + if (!end_write(pstub->fn, ws)) { throw("stub reset memory protect to r+x failed"); } @@ -530,6 +555,91 @@ class Stub return; } private: + struct WriteSession + { + unsigned char* write_addr; +#if defined(__APPLE__) + mach_vm_address_t alias; + mach_vm_size_t region_size; +#endif + }; + + bool begin_write(unsigned char* fn, WriteSession& ws) + { +#if defined(__APPLE__) + // On Apple Silicon (and on hardened runtime Intel Macs) the __TEXT + // segment cannot be made writable via mprotect. Obtain a writable + // alias of the target page(s) via mach_vm_remap + VM_INHERIT_SHARE, + // then escalate the alias to PROT_READ|PROT_WRITE. The alias shares + // the same physical memory as the original executable mapping, so + // writes through the alias are visible at the original address. + // + // Prerequisite (one-time, per-binary): the __TEXT segment's maxprot + // must allow write. See tool/macos_enable_stub.sh which patches the + // Mach-O header and re-signs the binary ad-hoc. + long ps = m_pagesize; + mach_vm_address_t page_start = (mach_vm_address_t)(uintptr_t)fn & ~((mach_vm_address_t)ps - 1); + mach_vm_size_t offset = (mach_vm_size_t)((uintptr_t)fn - page_start); + mach_vm_size_t region_size = ((offset + (mach_vm_size_t)CODESIZE + ps - 1) / ps) * ps; + mach_vm_address_t alias = 0; + vm_prot_t cur_prot = 0; + vm_prot_t max_prot = 0; + kern_return_t kr = mach_vm_remap(mach_task_self(), &alias, region_size, 0, + VM_FLAGS_ANYWHERE, mach_task_self(), page_start, FALSE, + &cur_prot, &max_prot, VM_INHERIT_SHARE); + if (kr != KERN_SUCCESS) + { + return false; + } + kr = mach_vm_protect(mach_task_self(), alias, region_size, FALSE, + VM_PROT_READ | VM_PROT_WRITE); + if (kr != KERN_SUCCESS) + { + mach_vm_deallocate(mach_task_self(), alias, region_size); + return false; + } + ws.alias = alias; + ws.region_size = region_size; + ws.write_addr = (unsigned char*)(uintptr_t)(alias + offset); + return true; +#elif defined(_WIN32) + DWORD lpflOldProtect; + if (0 == VirtualProtect(pageof(fn), m_pagesize * 2, PAGE_EXECUTE_READWRITE, &lpflOldProtect)) + { + return false; + } + ws.write_addr = fn; + return true; +#else + if (-1 == mprotect(pageof(fn), m_pagesize * 2, PROT_READ | PROT_WRITE | PROT_EXEC)) + { + return false; + } + ws.write_addr = fn; + return true; +#endif + } + + bool end_write(unsigned char* fn, WriteSession& ws) + { +#if defined(__APPLE__) + (void)fn; + if (ws.alias) + { + mach_vm_deallocate(mach_task_self(), ws.alias, ws.region_size); + ws.alias = 0; + } + return true; +#elif defined(_WIN32) + (void)ws; + DWORD lpflOldProtect; + return 0 != VirtualProtect(pageof(fn), m_pagesize * 2, PAGE_EXECUTE_READ, &lpflOldProtect); +#else + (void)ws; + return -1 != mprotect(pageof(fn), m_pagesize * 2, PROT_READ | PROT_EXEC); +#endif + } + void *pageof(unsigned char* addr) { #ifdef _WIN32 diff --git a/test/Makefile.darwin.clang b/test/Makefile.darwin.clang new file mode 100644 index 0000000..ee2b142 --- /dev/null +++ b/test/Makefile.darwin.clang @@ -0,0 +1,32 @@ +# Darwin (macOS) makefile - works on both Apple Silicon (arm64) and Intel (x86_64). +# +# macOS refuses to let mprotect raise __TEXT above r-x, and on Apple Silicon +# the kernel additionally enforces W^X at the page-table level. cpp-stub +# copes with the second restriction in code (see src/stub.h, the __APPLE__ +# branch), but we still need to lift __TEXT's maxprot to rwx in every built +# binary so the runtime patcher is allowed to create a writable alias of +# the text segment. That is done by ../tool/macos_enable_stub.sh, which +# also re-signs the binary ad-hoc. +# +# Tests that rely on Linux/Windows-specific behaviour (ptrace / PE headers +# / glibc internals / ELF-only tricks) are excluded. + +SOURCE = $(filter-out %win.cpp %_linux.cpp test_addr_any_linux.cpp, $(wildcard *.cpp)) +SOURCE := $(filter-out test_virtual_function_linux.cpp test_virtual_overload_function_linux.cpp, $(SOURCE)) +TARGETS = $(patsubst %.cpp, %, $(SOURCE)) +INC = -I../src -I../src_darwin +LIB = -lm +CC = clang++ +CFLAGS = -std=c++11 -g -fno-stack-protector -Wall -Wno-unused-function -Wno-unused-variable -Wno-unused-private-field + +ENABLE_STUB = ../tool/macos_enable_stub.sh + +all: clean $(TARGETS) + +$(TARGETS):%:%.cpp + $(CC) $< $(CFLAGS) $(INC) $(LIB) -o $@ + $(ENABLE_STUB) $@ + +.PHONY: clean all +clean: + -rm -rf $(TARGETS) diff --git a/tool/macos_enable_stub.sh b/tool/macos_enable_stub.sh new file mode 100755 index 0000000..0495cf1 --- /dev/null +++ b/tool/macos_enable_stub.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# macos_enable_stub.sh +# +# Post-link step required on macOS (both Intel and Apple Silicon) so that a +# binary using cpp-stub can actually patch its own __TEXT segment at runtime. +# +# macOS does not let mprotect raise __TEXT above its maxprot field (r-x by +# default) and, additionally, Apple Silicon enforces W^X on __TEXT at the +# page-table level. cpp-stub works around the W^X enforcement by writing to +# the __TEXT segment through a remapped writable alias (see src/stub.h, the +# __APPLE__ branch). For the kernel to allow the alias to be made writable, +# maxprot must first be escalated to rwx, which is what this script does. +# +# Because the Mach-O contents change, the existing code signature is +# invalidated; the binary is therefore re-signed with an ad-hoc signature. +# +# Usage: tool/macos_enable_stub.sh [ ...] + +set -e + +if [ "$#" -lt 1 ]; then + echo "usage: $0 [ ...]" >&2 + exit 1 +fi + +patch_one() { + local bin="$1" + if [ ! -f "$bin" ]; then + echo "skip: not a file: $bin" >&2 + return + fi + + # Compute the file offset of the __TEXT segment's maxprot field by + # walking the Mach-O load commands. This is portable across arm64 and + # x86_64 binaries (their header layouts differ). + local offset + offset=$(python3 - "$bin" <<'PY' +import struct, sys +path = sys.argv[1] +with open(path, 'rb') as f: + data = f.read() +magic = struct.unpack_from('/dev/null 2>&1 + + echo "enabled cpp-stub for: $bin" +} + +for bin in "$@"; do + patch_one "$bin" +done From 4333fde081a617821a5a47ff6c174efb4f8f0bf3 Mon Sep 17 00:00:00 2001 From: Jing Sima Date: Fri, 17 Apr 2026 16:24:15 +0800 Subject: [PATCH 2/2] Auto-wire the macOS post-link step from the build system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Downstream users no longer need to invoke tool/macos_enable_stub.sh themselves. The repo now ships: * CMakeLists.txt — INTERFACE target cpp-stub. When add_subdirectory'd, a cmake_language(DEFER) callback runs at end-of-config, finds every executable that links cpp-stub, and attaches the enable-stub post-build step automatically on Apple. A cpp_stub_enable(target) helper is exposed for the transitive-link corner case. * mk/cpp-stub.mk — Makefile fragment. Defines CPP_STUB_INCLUDE and a CPP_STUB_POSTLINK macro that expands to the enable-stub invocation on macOS and to ':' (no-op) elsewhere, so one rule works on every platform. * example/cmake_smoke/ — self-contained CMake project that just links cpp-stub and expects the patch+sign to happen with zero manual steps. Exercised by CI on both macos-13 and macos-latest. * test/Makefile.darwin.clang now dogfoods mk/cpp-stub.mk. * README.md / README_zh.md gain a 'macOS integration' section showing the zero-step CMake path, the one-line Make path, and a snippet for Xcode/Bazel/other build systems. * .github/workflows/make-test-multi-platform.yml gains a second step per macOS runner that configures and runs example/cmake_smoke, proving end-to-end that a downstream CMake project need do nothing beyond target_link_libraries(... cpp-stub). Verified locally on macOS 26.4 arm64: CMake smoke + Makefile-based tests all pass, and the existing test matrix (test_function, test_class_member_function, test_variadic_function) is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../workflows/make-test-multi-platform.yml | 12 ++ CMakeLists.txt | 106 ++++++++++++++++++ README.md | 71 +++++++++++- README_zh.md | 65 ++++++++++- example/cmake_smoke/CMakeLists.txt | 13 +++ example/cmake_smoke/smoke.cpp | 33 ++++++ mk/cpp-stub.mk | 32 ++++++ test/Makefile.darwin.clang | 12 +- 8 files changed, 328 insertions(+), 16 deletions(-) create mode 100644 CMakeLists.txt create mode 100644 example/cmake_smoke/CMakeLists.txt create mode 100644 example/cmake_smoke/smoke.cpp create mode 100644 mk/cpp-stub.mk diff --git a/.github/workflows/make-test-multi-platform.yml b/.github/workflows/make-test-multi-platform.yml index 1e1fce5..3c21994 100644 --- a/.github/workflows/make-test-multi-platform.yml +++ b/.github/workflows/make-test-multi-platform.yml @@ -87,3 +87,15 @@ jobs: done working-directory: ${{ github.workspace }}/test shell: bash + + - name: CMake integration smoke test (macOS) + if: ${{ startsWith(matrix.os, 'macos') }} + run: | + # Verifies that downstream projects only need + # target_link_libraries(my_test PRIVATE cpp-stub) + # and no manual post-build step — the root CMakeLists.txt + # defers and attaches macos_enable_stub.sh automatically. + cmake -S example/cmake_smoke -B example/cmake_smoke/build + cmake --build example/cmake_smoke/build + ./example/cmake_smoke/build/cmake_smoke + shell: bash diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c2e9f53 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,106 @@ +cmake_minimum_required(VERSION 3.19) +project(cpp-stub CXX) + +# Header-only interface library. Downstream usage: +# +# add_subdirectory(third_party/cpp-stub) +# add_executable(my_test test.cpp) +# target_link_libraries(my_test PRIVATE cpp-stub) +# +# On macOS the build system automatically patches the produced binary +# after link so cpp-stub can rewrite __TEXT at runtime; no manual step +# is required. On Linux/Windows the post-build hook is a no-op. + +add_library(cpp-stub INTERFACE) +add_library(cpp-stub::cpp-stub ALIAS cpp-stub) + +target_include_directories(cpp-stub INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/src") + +if(APPLE) + target_include_directories(cpp-stub INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/src_darwin") +endif() + +if(UNIX AND NOT APPLE) + target_include_directories(cpp-stub INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/src_linux") +endif() + +if(WIN32) + target_include_directories(cpp-stub INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/src_win") +endif() + +# ----------------------------------------------------------------------------- +# macOS post-build automation +# ----------------------------------------------------------------------------- +if(APPLE) + set(_CPP_STUB_ENABLE_TOOL + "${CMAKE_CURRENT_SOURCE_DIR}/tool/macos_enable_stub.sh" + CACHE INTERNAL "") + + # Explicit opt-in helper for cases where the auto-detect below cannot + # see a target (e.g. the executable only links cpp-stub transitively + # through an intermediate static library). + function(cpp_stub_enable target) + get_target_property(_already "${target}" _CPP_STUB_POSTBUILD_ADDED) + if(_already) + return() + endif() + add_custom_command(TARGET "${target}" POST_BUILD + COMMAND "${_CPP_STUB_ENABLE_TOOL}" "$" + COMMENT "cpp-stub: patching __TEXT maxprot and re-signing ${target}" + VERBATIM) + set_target_properties("${target}" PROPERTIES + _CPP_STUB_POSTBUILD_ADDED TRUE) + endfunction() + + # Recursively collect all targets in the directory tree rooted at `dir`. + function(_cpp_stub_collect_targets dir out_var) + set(_acc "") + get_property(_here DIRECTORY "${dir}" PROPERTY BUILDSYSTEM_TARGETS) + if(_here) + list(APPEND _acc ${_here}) + endif() + get_property(_subs DIRECTORY "${dir}" PROPERTY SUBDIRECTORIES) + foreach(_s IN LISTS _subs) + _cpp_stub_collect_targets("${_s}" _sub) + if(_sub) + list(APPEND _acc ${_sub}) + endif() + endforeach() + set(${out_var} "${_acc}" PARENT_SCOPE) + endfunction() + + # Deferred callback: invoked at end-of-config of the top-level directory, + # by which time every target has been declared. For each executable that + # (directly) links cpp-stub, attach the post-build patch step. + function(_cpp_stub_auto_enable_all) + _cpp_stub_collect_targets("${CMAKE_SOURCE_DIR}" _targets) + foreach(_t IN LISTS _targets) + if(NOT TARGET "${_t}") + continue() + endif() + get_target_property(_type "${_t}" TYPE) + if(NOT _type STREQUAL "EXECUTABLE") + continue() + endif() + set(_links "") + get_target_property(_l "${_t}" LINK_LIBRARIES) + if(_l) + list(APPEND _links ${_l}) + endif() + get_target_property(_il "${_t}" INTERFACE_LINK_LIBRARIES) + if(_il) + list(APPEND _links ${_il}) + endif() + if("cpp-stub" IN_LIST _links OR "cpp-stub::cpp-stub" IN_LIST _links) + cpp_stub_enable("${_t}") + endif() + endforeach() + endfunction() + + cmake_language(DEFER DIRECTORY "${CMAKE_SOURCE_DIR}" + CALL _cpp_stub_auto_enable_all) +endif() diff --git a/README.md b/README.md index c9b9d55..0ca969c 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,14 @@ - Supported operating systems: * [x] Windows * [x] Linux - * [x] MacOS — both Intel (x86-64) and Apple Silicon (arm64). After building, run - `tool/macos_enable_stub.sh ` on each test executable to lift the - `__TEXT` segment's `maxprot` to `rwx` and ad-hoc re-sign (required by - macOS's W^X enforcement; see [issue #49](https://github.com/coolxv/cpp-stub/issues/49) - and [test/Makefile.darwin.clang](test/Makefile.darwin.clang) for an example). + * [x] MacOS — both Intel (x86-64) and Apple Silicon (arm64). Because macOS + enforces W^X on `__TEXT` and caps `mprotect` at each segment's `maxprot`, + every binary that uses cpp-stub needs a tiny post-link adjustment + (lift `__TEXT.maxprot` to `rwx` and ad-hoc re-sign; see + [issue #49](https://github.com/coolxv/cpp-stub/issues/49)). + cpp-stub ships build-system helpers that do this automatically, so + downstream users normally do **not** have to call anything by hand. + See [the macOS integration section](#macos-integration) below. - Supported hardware platform: * [x] x86 * [x] x86-64 @@ -72,6 +75,64 @@ - -fprofile-arcs - -ftest-coverage +## macOS integration + +macOS requires every binary that uses cpp-stub to have its `__TEXT` +segment's `maxprot` lifted to `rwx` and be ad-hoc re-signed once, after +link. The library ships helpers that wire this up for you; on Linux and +Windows the same helpers do nothing. + +### CMake (zero manual steps) + +```cmake +add_subdirectory(third_party/cpp-stub) +add_executable(my_test test.cpp) +target_link_libraries(my_test PRIVATE cpp-stub) +``` + +The root `CMakeLists.txt` installs a deferred hook that finds every +executable that links `cpp-stub` and automatically attaches the +post-build patch step on Apple platforms. For the rare case where an +executable only links cpp-stub *transitively* through a static library, +call it explicitly: + +```cmake +cpp_stub_enable(my_test) +``` + +### Makefile + +```make +CPP_STUB_DIR := third_party/cpp-stub +include $(CPP_STUB_DIR)/mk/cpp-stub.mk + +my_test: my_test.cpp + $(CXX) $(addprefix -I,$(CPP_STUB_INCLUDE)) ... -o $@ $< + @$(CPP_STUB_POSTLINK) +``` + +`$(CPP_STUB_POSTLINK)` expands to the enable-stub command on macOS and +to a no-op (`:`) on Linux/Windows, so the same rule works cross-platform. + +### Xcode / Bazel / other + +Add a run-script / `genrule` that invokes the helper once per built +executable: + +```bash +third_party/cpp-stub/tool/macos_enable_stub.sh "$TARGET_BUILD_DIR/$EXECUTABLE_PATH" +``` + +### Why this step exists + +See [issue #49](https://github.com/coolxv/cpp-stub/issues/49) and the +`__APPLE__` branch of [src/stub.h](src/stub.h) for the full story: the +kernel will not let `mprotect`/`mach_vm_protect` raise `__TEXT` above +its Mach-O `maxprot`, and Apple Silicon further enforces W^X at the +page-table level even when `maxprot=rwx`. `tool/macos_enable_stub.sh` +raises `maxprot` so that cpp-stub can obtain a writable alias of +`__TEXT` at runtime via `mach_vm_remap`. + ## Code coverage statistics for linux g++ ``` lcov -d build/ -z diff --git a/README_zh.md b/README_zh.md index ddcff14..437c989 100644 --- a/README_zh.md +++ b/README_zh.md @@ -9,11 +9,12 @@ - 支持的操作系统 : * [x] Windows * [x] Linux - * [x] MacOS —— 同时支持 Intel (x86-64) 与 Apple Silicon (arm64)。编译完成后, - 对每个可执行文件执行 `tool/macos_enable_stub.sh `,把 `__TEXT` - 段的 `maxprot` 抬到 `rwx` 并做 ad-hoc 重新签名(macOS 的 W^X 强制保护 - 要求此步骤;参考 [issue #49](https://github.com/coolxv/cpp-stub/issues/49) - 以及 [test/Makefile.darwin.clang](test/Makefile.darwin.clang) 里的示例)。 + * [x] MacOS —— 同时支持 Intel (x86-64) 与 Apple Silicon (arm64)。由于 macOS + 对 `__TEXT` 段施加 W^X 限制,并且 `mprotect` 不能超过段的 `maxprot`, + 使用 cpp-stub 的每个可执行文件都需要在链接后做一次小处理(把 `__TEXT.maxprot` + 抬到 `rwx` 并 ad-hoc 重新签名,见 [issue #49](https://github.com/coolxv/cpp-stub/issues/49))。 + 本仓库提供了构建系统集成助手,下游通常**不需要手动调用**任何脚本。 + 详见下文的 [macOS 集成说明](#macos-集成说明)。 - 支持的硬件平台 : * [x] x86 @@ -73,6 +74,60 @@ - -fprofile-arcs - -ftest-coverage +## macOS 集成说明 + +macOS 下每个使用 cpp-stub 的可执行文件都需要在链接完成后做一次小处理: +把 `__TEXT.maxprot` 抬到 `rwx`,并用 ad-hoc 重新签名。本仓库自带构建系统 +助手帮你自动做这件事;Linux / Windows 下这些助手是空操作。 + +### CMake(零手动步骤) + +```cmake +add_subdirectory(third_party/cpp-stub) +add_executable(my_test test.cpp) +target_link_libraries(my_test PRIVATE cpp-stub) +``` + +仓库根部的 `CMakeLists.txt` 会注册一个 deferred hook:在配置阶段结束时 +遍历所有 executable,对每个直接链接 `cpp-stub` 的目标自动挂 POST_BUILD +步骤。只有当可执行文件通过中间静态库**间接**链接 cpp-stub 时,才需要 +显式调用: + +```cmake +cpp_stub_enable(my_test) +``` + +### Makefile + +```make +CPP_STUB_DIR := third_party/cpp-stub +include $(CPP_STUB_DIR)/mk/cpp-stub.mk + +my_test: my_test.cpp + $(CXX) $(addprefix -I,$(CPP_STUB_INCLUDE)) ... -o $@ $< + @$(CPP_STUB_POSTLINK) +``` + +`$(CPP_STUB_POSTLINK)` 在 macOS 上展开为 enable-stub 命令, +在 Linux/Windows 上展开为空操作 (`:`),同一份 Makefile 跨平台可用。 + +### Xcode / Bazel / 其它 + +加一条 run-script / `genrule`,对每个可执行文件调一次: + +```bash +third_party/cpp-stub/tool/macos_enable_stub.sh "$TARGET_BUILD_DIR/$EXECUTABLE_PATH" +``` + +### 为什么需要这一步 + +参见 [issue #49](https://github.com/coolxv/cpp-stub/issues/49) 以及 +[src/stub.h](src/stub.h) 的 `__APPLE__` 分支:内核不允许 +`mprotect`/`mach_vm_protect` 把 `__TEXT` 的权限提升到 Mach-O 里 +`maxprot` 之上,而 Apple Silicon 还在页表层面强制 W^X。 +`tool/macos_enable_stub.sh` 负责把 `maxprot` 抬到 `rwx`,这样 +cpp-stub 运行时才能通过 `mach_vm_remap` 拿到 `__TEXT` 的可写别名。 + ## 代码覆盖率, linux g++使用方法 ``` diff --git a/example/cmake_smoke/CMakeLists.txt b/example/cmake_smoke/CMakeLists.txt new file mode 100644 index 0000000..ded6eca --- /dev/null +++ b/example/cmake_smoke/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.19) +project(cpp_stub_cmake_smoke CXX) + +set(CMAKE_CXX_STANDARD 11) + +# Points at repository root (two levels up from this file). +add_subdirectory(../.. cpp-stub-build) + +add_executable(cmake_smoke smoke.cpp) +target_link_libraries(cmake_smoke PRIVATE cpp-stub) + +# Deliberately no cpp_stub_enable() call here: verifies that merely +# linking the interface target is enough on macOS. diff --git a/example/cmake_smoke/smoke.cpp b/example/cmake_smoke/smoke.cpp new file mode 100644 index 0000000..2c8aab0 --- /dev/null +++ b/example/cmake_smoke/smoke.cpp @@ -0,0 +1,33 @@ +#include +#include "stub.h" + +// cpp-stub's arm64 trampoline is 16 bytes. Keep these two functions well +// bigger than that and noinline so the linker won't place one inside the +// other's patch range. +__attribute__((noinline)) int foo() { + std::cout << "I am foo" << std::endl; + return 1; +} + +__attribute__((noinline)) int foo_stub() { + std::cout << "I am foo_stub" << std::endl; + return 42; +} + +int main() { + Stub s; + s.set(foo, foo_stub); + int v = foo(); + if (v != 42) { + std::cerr << "FAIL: after set, foo()=" << v << ", expected 42\n"; + return 1; + } + s.reset(foo); + v = foo(); + if (v != 1) { + std::cerr << "FAIL: after reset, foo()=" << v << ", expected 1\n"; + return 1; + } + std::cout << "cmake_smoke: OK" << std::endl; + return 0; +} diff --git a/mk/cpp-stub.mk b/mk/cpp-stub.mk new file mode 100644 index 0000000..910c0d1 --- /dev/null +++ b/mk/cpp-stub.mk @@ -0,0 +1,32 @@ +# cpp-stub Makefile helper. +# +# Usage: +# CPP_STUB_DIR := third_party/cpp-stub +# include $(CPP_STUB_DIR)/mk/cpp-stub.mk +# +# my_test: my_test.cpp +# $(CXX) -I$(CPP_STUB_INCLUDE) ... -o $@ $< +# $(CPP_STUB_POSTLINK) +# +# On macOS $(CPP_STUB_POSTLINK) patches the produced binary's __TEXT +# maxprot to rwx and ad-hoc re-signs it so cpp-stub can rewrite code +# pages at runtime. On Linux/Windows it is a no-op, so the same +# Makefile works unchanged across platforms. + +# Resolve this file's directory regardless of how the user included it. +_CPP_STUB_MK_DIR := $(dir $(lastword $(MAKEFILE_LIST))) +CPP_STUB_ROOT ?= $(abspath $(_CPP_STUB_MK_DIR)/..) +CPP_STUB_INCLUDE ?= $(CPP_STUB_ROOT)/src + +_CPP_STUB_UNAME := $(shell uname -s 2>/dev/null) + +ifeq ($(_CPP_STUB_UNAME),Darwin) +CPP_STUB_INCLUDE += $(CPP_STUB_ROOT)/src_darwin +CPP_STUB_POSTLINK = $(CPP_STUB_ROOT)/tool/macos_enable_stub.sh $@ +else ifeq ($(_CPP_STUB_UNAME),Linux) +CPP_STUB_INCLUDE += $(CPP_STUB_ROOT)/src_linux +CPP_STUB_POSTLINK = : +else +# Windows / other: no-op +CPP_STUB_POSTLINK = : +endif diff --git a/test/Makefile.darwin.clang b/test/Makefile.darwin.clang index ee2b142..2114209 100644 --- a/test/Makefile.darwin.clang +++ b/test/Makefile.darwin.clang @@ -5,27 +5,27 @@ # copes with the second restriction in code (see src/stub.h, the __APPLE__ # branch), but we still need to lift __TEXT's maxprot to rwx in every built # binary so the runtime patcher is allowed to create a writable alias of -# the text segment. That is done by ../tool/macos_enable_stub.sh, which -# also re-signs the binary ad-hoc. +# the text segment. That is done by ../tool/macos_enable_stub.sh, wired in +# here through the shared cpp-stub.mk helper (which also re-signs ad-hoc). # # Tests that rely on Linux/Windows-specific behaviour (ptrace / PE headers # / glibc internals / ELF-only tricks) are excluded. +include ../mk/cpp-stub.mk + SOURCE = $(filter-out %win.cpp %_linux.cpp test_addr_any_linux.cpp, $(wildcard *.cpp)) SOURCE := $(filter-out test_virtual_function_linux.cpp test_virtual_overload_function_linux.cpp, $(SOURCE)) TARGETS = $(patsubst %.cpp, %, $(SOURCE)) -INC = -I../src -I../src_darwin +INC = $(addprefix -I,$(CPP_STUB_INCLUDE)) LIB = -lm CC = clang++ CFLAGS = -std=c++11 -g -fno-stack-protector -Wall -Wno-unused-function -Wno-unused-variable -Wno-unused-private-field -ENABLE_STUB = ../tool/macos_enable_stub.sh - all: clean $(TARGETS) $(TARGETS):%:%.cpp $(CC) $< $(CFLAGS) $(INC) $(LIB) -o $@ - $(ENABLE_STUB) $@ + @$(CPP_STUB_POSTLINK) .PHONY: clean all clean: