通过 dyld-interposing 实现C/C++代码注入

苹果系统的链接器/usr/lib/dyld 提供了一个叫dyld-interposing的功能(从 Mac OS X 10.4 开始),可以在程序启动时替换掉某个函数的实现。这个功能可以用来实现代码注入(详见:《Mac OS X Internals: A Systems Approach》- Amit Singh - 第二章 2.6.3.4 dyld interposing)

举个栗子

比如,我们可以在程序运行时,替换掉malloc函数的实现:

malloc_trace.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// malloc_trace.c
#include <stdio.h>
#include <stdlib.h>

#include <mach-o/dyld-interposing.h>
#include <memory.h> // memset
#include <malloc/malloc.h> // malloc_printf

void *trace_malloc(size_t size) {
char *p = malloc(size);
// fills with '#'
memset(p, '#', size);
malloc_printf("malloc(%u) = %p\n", size, p);
return (void *)p;
}

DYLD_INTERPOSE(trace_malloc, malloc);
test.c
1
2
3
4
5
6
7
8
9
// test.c
#include <stdio.h>
#include <stdlib.h>
int main() {
char *p = (char*)malloc(10);
printf("malloc return %p, %s\n", p, p);
free(p);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ cc -dynamiclib -o libmalloctrace.dylib malloc_trace.c -install_name libmalloctrace.dylib
$ cc -o test test.c
$ DYLD_INSERT_LIBRARIES=libmalloctrace.dylib ./test

test(46555,0x11bdd3600) malloc: malloc(1536) = 0x7febbc808200
test(46555,0x11bdd3600) malloc: malloc(32) = 0x7febbc704130
test(46555,0x11bdd3600) malloc: malloc(32) = 0x7febbc704170
test(46555,0x11bdd3600) malloc: malloc(20) = 0x7febbc705550
test(46555,0x11bdd3600) malloc: malloc(422) = 0x7febbc7055d0
test(46555,0x11bdd3600) malloc: malloc(50) = 0x7febbc7057e0
test(46555,0x11bdd3600) malloc: malloc(16) = 0x7febbc705880
test(46555,0x11bdd3600) malloc: malloc(52) = 0x7febbc705900
test(46555,0x11bdd3600) malloc: malloc(12) = 0x7febbc7059b0
test(46555,0x11bdd3600) malloc: malloc(10) = 0x7febbc705b00
test(46555,0x11bdd3600) malloc: malloc(4096) = 0x7febbc808800
malloc return 0x7febbc705b00, ##########

Graphics API Debuggers

Learning Render Graph

什么是 Render Graph?

Render Graph 或者说 Frame graph 是对复杂渲染管线的一个高度抽象,以图(Graph)的形式呈现渲染过程中的各个步骤,不同的渲染任务之间的依赖关系,以及它们对资源(如纹理、缓冲区等)的使用。

Frame graphs are a design pattern for handling complex rendering pipelines, which are currently used in industry. Their usage is motivated by handling barriers, queue synchronization and memory aliasing in the background by abstracting the rendering pipeline of a frame on a higher level.
—— https://github.com/gfx-rs/gfx/wiki/Frame-graphs

解决什么问题?

在传统的渲染管线中,渲染过程通常被划分为多个阶段,如下图所示:

这些阶段之间存在着输入和输出的依赖关系,其中一个阶段的输出作为下一个阶段的输入

Render Graph 的主要思想是将渲染过程表示为一个有向无环图(DAG),其中节点表示渲染通道(Render pass),边表示依赖关系。每个渲染通道执行特定的渲染操作,可具有输入和输出资源,例如Texture、Frame Buffer和执行的 Shader/Program。例如,假设节点 A 的输出Texture是节点 B 的输入Texture,那么节点 B 就依赖于节点 A。

通过概括渲染流程中的依赖关系,确保渲染阶段按正确的顺序执行,并且在需要时可基于一定的同步机制(Fence、Semaphore、Resource barriers)尽可能地并行执行渲染通道。

Render Graph 的目标是为了解决大型渲染引擎里复杂渲染管线中的一些问题。如资源生命周期管理、渲染效率、渲染过程的可视化调试等等。

Render Graph 不仅在游戏引擎中广泛应用:

它的理念也在现代图形 API 中可窥一斑,如VulkanRender PassDirectX 12Command ListMetalRender Pass等。

从Android 原生库 (.so) 中里挖掘一些有用的信息

当一个 Android APP 需要集成别的地方来的原生库(.so)时,你可能也会跟我一样会有那么几点疑惑:

  • 这个 so 用的什么 NDK 版本编译的?会不会跟项目里其它的so 冲突,尤其项目里使用共享 C++ STL的情况下 ANDROID_STL=c++_shared,一个应用不能使用多个 C++ 运行时
  • 这个 so 目标 Android API 等级是多少?会不会大于项目的minSdkVersion
  • 这个 so 依赖(链接)其它哪些 so?这些 so 有没有都放进项目里?
  • 这个 so 有没有除了用文件哈希之外唯一编号,用来标识崩溃堆栈等?

ps. 本文假定读者有一定 Android Native 开发经验,且理解一些基本的概念。

查看 so 的 NDK 版本信息

通过 readelf 工具查看 Android NDK 编译出来的 so 的 Section headers 里有什么 Android 特有的玩意。

ps. 可以用 ndk-which 找到 NDK 中预编译好的 readelf

1
2
$ $ANDROID_NDK_HOME/ndk-which --abi arm64-v8a readelf
/~/ndk/21.4.7075529/prebuilt/darwin-x86_64/bin/../../../toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android-readelf

定义一个名为 readelf 的 alias 方便在 Terminal 中调用 aarch64-linux-android-readelf

1
2
3
4
5
$ alias readelf=`$ANDROID_NDK_HOME/ndk-which --abi arm64-v8a readelf`
$ readelf -v
GNU readelf (GNU Binutils) 2.27.0.20170315
Copyright (C) 2016 Free Software Foundation, Inc.
...

以 NDK 中带的 libc++_shared.so 为例,在我本机上路径是$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so

1
$ readelf -WS $ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so

How to run Java standalone app (with JNI) on Android without creating an apk

In this week, I found a great POC to run a pure Java standalone app (command line tool, no apk) on Android. But what about running a standalone application using JNI (with .so files) on Android like this?

Java app with JNI

Imagine there is a Java program that loads the JNI shared native library to run and use some Android APIs :

HelloWorld.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example;
import android.os.Build;
import android.util.Log;
public class Helloworld {

static { System.loadLibrary("hello"); }
public static native String stringFromJNI();

public static void main(String[] args) {
Log.i("@@", "Hello world, " + Build.MANUFACTURER + " "+ Build.MODEL + "!");
Log.i("@@", stringFromJNI());
System.out.println(stringFromJNI());
System.out.println("DONE.");
}

public static String getBuildVersion() {
return Build.VERSION.RELEASE;
}
}

…the JNI source would be like:

hello-jni.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ... emit codes

JNIEXPORT jstring JNICALL
Java_com_example_Helloworld_stringFromJNI(JNIEnv *env,
jobject thiz)
{
// ... emit codes
jmethodID versionFunc = (*env)->GetStaticMethodID(env, clz, "getBuildVersion", "()Ljava/lang/String;");

jstring buildVersion = (*env)->CallStaticObjectMethod(env, clz, versionFunc);
const char *version = (*env)->GetStringUTFChars(env, buildVersion, NULL);

if (!version)
{
LOGE("Unable to get version string");
}
else
{
LOGI("Build Version - %s\n", version);
(*env)->ReleaseStringUTFChars(env, buildVersion, version);
}
(*env)->DeleteLocalRef(env, buildVersion);

return (*env)->NewStringUTF(env,
"Hello from JNI ! Compiled with ABI " ABI ".");
}
// ...

The working directory structure

1
2
3
.
├── Helloworld.java
└── hello-jni.c

Compile and deploy

Now we need to compile both the Java and C sources for Android.

Using javac and dx to compile for a jar file which Android can read:

1
2
3
4
5
6
7
export BUILD_DIR=$PWD/build
export JARFILE=helloworld.jar
export JAVAC_OPTS=-source 1.8 -target 1.8 -cp .:$ANDROID_HOME/platforms/android-30/android.jar
# Compile .java to .class
javac $JAVAC_OPTS -d $BUILD_DIR/classes Helloworld.java
# Convert .class file into a dex file and embedded in a jar file
$ANDROID_HOME/build-tools/30.0.2/dx --output=$BUILD_DIR/$JARFILE --dex ./$BUILD_DIR/classes

Cross-compile the C to Android shared native library via NDK:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Using the prebuilt toolchain diretly
# See https://developer.android.com/ndk/guides/other_build_systems
export ANDROID_NDK_STANDALONE=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64
$ANDROID_NDK_STANDALONE/bin/clang \
--target=aarch64-none-linux-android21 \
--gcc-toolchain=$ANDROID_NDK_STANDALONE \
--sysroot $ANDROID_NDK_STANDALONE/sysroot \
-L${ANDROID_NDK_STANDALONE}/sysroot/usr/lib \
-shared -g -DANDROID -fdata-sections -ffunction-sections -funwind-tables \
-fstack-protector-strong -no-canonical-prefixes -fno-addrsig -fPIC \
-Wl,-llog \
-Wl,-soname,libhello.so \
-o $BUILD_DIR/libhello.so hello-jni.c

The build directory looks like:

1
2
3
4
5
6
7
build
├── classes
│ └── com
│ └── example
│ └── Helloworld.class
├── helloworld.jar
└── libhello.so

Using the adb tool to deploy the helloworld.jar and libhello.so to Android device:

1
2
adb shell mkdir /data/local/tmp/helloworld
adb push $BUILD_DIR/helloworld.jar $BUILD_DIR/libhello.so /data/local/tmp/helloworld/

Run appliation on Android

Run helloworld.jar via app_process on Android:

1
2
3
4
5
6
7
8
adb shell CLASSPATH="/data/local/tmp/helloworld/helloworld.jar" \
LD_LIBRARY_PATH=/data/local/tmp/helloworld \
app_process \
/data/local/tmp/helloworld \
com.example.Helloworld
# output
Hello from JNI ! Compiled with ABI arm64-v8a.
DONE.

How to detect memory leaks in C++ programs on macOS

一段很典型的内存泄漏C++ 代码如下:

1
2
3
4
5
6
7
8
int main(int argc, const char **argv)
{
auto *p = new int(10);
// other codes ...
p = nullptr; // leaked
// ...
return 0;
}

如何在庞大的 C++ 项目代码中找出类似的问题呢?

libgmalloc

苹果提供了内存调试功能:Guard Malloc,用于debug 内存问题。 man libgmalloc 可以查看更多使用信息。

开启libgmalloc的记录 malloc 调用日志的功能,在执行程序前设置环境变量MallocStackLogging=1,如:

1
MallocStackLogging=1 ./my_tool

日志会写到一个临时文件中:

my_tool(38364) MallocStackLogging: stack logs being written to /private/tmp/stack-logs.38364.103f3a000.my_tool.19n2JH.index
my_tool(38364) MallocStackLogging: recording malloc and VM allocation stacks to disk using standard recorder

需要注意的是,在程序退出时,这个调用日志文件会被自动删除:

my_tool(38364) MallocStackLogging: stack logs deleted from /private/tmp/stack-logs.38364.103f3a000.my_tool.19n2JH.index

所以需要将程序在结束时最好 block 住,以便分析日志文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
static void wait_for_input()
{
std::cout << "Press Enter to exit." << std::endl;
char b[1];
std::cin.read(b, 1);
}
int main(int argc, const char **argv)
{
auto *p = new int(10);
// other codes ...
p = nullptr; // leaked
// ...

// blocking program for analyzing malloc stack history
wait_for_input();
return 0;
}

The leaks Tools

另外,可以直接用 macOS 检测内存泄漏:/usr/bin/leaks (详见:the leaks Tool

终端中执行 man leaks 查看使用手册。

leaks 使用方式很简单,指定 pid 即可 attach 到执行中的程序:

1
2
3
4
# pid=38364
leaks $pid --outputGraph=$pid.memgraph
# open memory graph file with Xcode
open $pid.memgraph

Xcode Memory Graph Debugger

结合起来就是:

  1. 修改 C++ 程序 main 函数,使其在结束时 block,重编程序
  2. 设置环境变量MallocStackLogging=1,执行程序。
  3. 在程序执行结束时,亦即 block 时,执行 leaks,保存 memory graph文件
  4. ⌃+C 结束程序
  5. 使用 Xcode Memory Graph Debugger 打开 memory graph文件,分析内存泄漏

Debug iOS app in Visual Studio Code

使用 LLDB 远程调试程序

调试分为本地调试和远程调试,iOS app 不管安装在模拟器还是真机,均属于远程调试。

这里简述一下大概的原理。

远程启动 lldb-server ,或者叫 debugserver, 比如使用 ios-deploy 工具开启真机的 debugserver 并将远程端口的 socket 数据转发(或者叫代理)到本机:

1
ios-deploy --id <device_id> --nolldb --json

输出如下:

1
2
3
4
5
6
7
8
9
10
{
"Event" : "PasswordProtectedStatus",
"Status" : 0
}{
"Event" : "MountDeveloperImage",
"SymbolsPath" : "\/~/Library\/Developer\/Xcode\/iOS DeviceSupport\/15.3.1 (19D52)\/Symbols"
}{
"Event" : "DebugServerLaunched",
"Port" : 61115
}

DebugServerLaunched 代表真机上的 debugserver 已启动成功,本地转发端口为 61115

ps. <device_id>是你的真机的id,用命令 ios-deploy --detect 可找到真机的id。

本地启动 lldb,先通过 platform select 对应平台(用于加载系统库和符号),之后通过process connect connect://127.0.0.1:<Port> ,通过本地转发的 socket 连接到远端 debugserver 。

之后通过 lldb 设定目标执行文件及其依赖的模块,及其远端文件路径映射,即可实现远程调试

命令行启动lldb,并输入如下:

1
2
3
4
(lldb) platform select remote-ios
(lldb) file myapp.app
(lldb) process connect connect://127.0.0.1:61115
(lldb) run

这时候你可能会发现报错了: error: No such file or directory (myapp.app)

因为,对于iOS app 你需要先通过ios-deploy安装到真机上之后,才能走通这个流程:

1
2
# 安装app
ios-deploy --id <device_id> --bundle my.app --json

How to explicitly control exported symbols of dynamic shared libraries

Benefits of exporting symbols on demand

  1. Reduce the amount of time of loading the library.
  2. Reduce the size of library.
  3. Optimize the code generation.
  4. Improve library maintainability and make the library easier to use.
  5. Reduce the potential for symbol collision.

List the symbols of shared library

1. nm

https://llvm.org/docs/CommandGuide/llvm-nm.html

nm -gD libxxx.so

2. objdump

https://llvm.org/docs/CommandGuide/llvm-objdump.html

objdump -T libxxx.so

3. readelf

https://man7.org/linux/man-pages/man1/readelf.1.html

readelf -Ws libxxx.so

Option 1. the visibility attribute

1
2
3
4
5
6
7
8
9
#if HAVE_VISIBILITY && BUILDING_LIBFOO
#define LIBFOO_EXPORTED __attribute__((__visibility__("default")))
#elif (defined _WIN32 && !defined __CYGWIN__) && BUILDING_SHARED && BUILDING_LIBFOO
#define LIBFOO_EXPORTED __declspec(dllexport)
#elif (defined _WIN32 && !defined __CYGWIN__) && BUILDING_SHARED
#define LIBFOO_EXPORTED __declspec(dllimport)
#else
#define LIBFOO_EXPORTED
#endif

Compile the code with -fvisibility=hidden flag. This option forces the default visibility of all symbols to be hidden.

1
c++ -I. -fvisibility=hidden -o a.o -c a.cpp

On Windows, using the keyword __declspec(dllexport) to export symbols . See https://learn.microsoft.com/en-us/cpp/build/exporting-from-a-dll-using-declspec-dllexport?view=msvc-170

See more details: https://gcc.gnu.org/wiki/Visibility

Android CMake 项目集成 Ccache

Ccache is a compiler cache. 能极大的提升 clean build 编译效率。

github 上有一个为 CMake 提供 Ccache 集成的项目:https://github.com/TheLartians/Ccache.cmake,可以拿过来直接用。

在 cpp 库根目录的 CMakeLists.txt 中添加Ccache.cmake,如:

src/main/cpp/CMakeLists.txt
1
2
3
4
5
6
7
8
9
10
11

cmake_minimum_required(VERSION 3.11.0)

project(myapp)

include(FetchContent)
FetchContent_Declare(Ccache.cmake
GIT_REPOSITORY https://github.com/TheLartians/Ccache.cmake.git
GIT_TAG origin/master
)
FetchContent_MakeAvailable(Ccache.cmake)

注意:FetchContent 最低要求 CMake 3.11

下载安装 Ccache: https://ccache.dev/download.html ,或者使用 Homebrew 安装:brew install ccache

安装完毕后,在 Android 项目的 build.gradle 中记得加上参数开启次模块功能:-DUSE_CCACHE=ON,并给Ccache 加上选项-DCCACHE_OPTIONS=CCACHE_CPP2=true;CCACHE_SLOPPINESS=time_macros,locale,file_stat_matches

build.gradle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}

android {
namespace 'com.example.myapplication'
compileSdk 33

defaultConfig {
applicationId "com.example.myapplication"
minSdk 21
targetSdk 33
versionCode 1
versionName "1.0"

externalNativeBuild {
cmake {
arguments "-DUSE_CCACHE=ON", "-DCCACHE_OPTIONS=CCACHE_CPP2=true;CCACHE_SLOPPINESS=time_macros,locale,file_stat_matches"
}
}
}

externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
version '3.22.1'

}
}

}

开启 ccache stats log,clean build,日志如下表示命中缓存:

# ~/MyApplication/app/src/main/cpp/native-lib.cpp
direct_cache_hit
local_storage_hit
local_storage_read_hit
local_storage_read_hit

ENJOY it.

沉迷 Midjourney

近些日子沉迷 Midjourney,用不同咒语来回调教 AI,也算体验了一把炼丹的乐趣 —— 投喂一堆奇奇怪怪的东西,得一个匪夷所思的玩意。

推荐一个 Notion 笔记,总结了非常多的风格、建筑、艺术家、摄影技法等等关键字,用来辅助调教:https://marigoldguide.notion.site/marigoldguide/52ac9968a8da4003a825039022561a30?v=057d3669790c4dc28bd8d3ddf35e3a37

比如:
Castle Claude Monet –seed 9875
Castle in the style of Claude Monet --seed 9875

另外还有一个可视化组织咒语的工具网站,也很好用:MidJourney Prompt Helper


下面是我用使用 Midjourney 画的图:

泉州蟳埔女,簪花围:

a pretty young woman smells flower on chair at sunset

/imagine prompt: https://s.mj.run/t3qtGrXY96c, a pretty young woman smells flower on chair at sunset, --q 2 --ar 9:16

蓝眼泪:

In the midnight, the ocean beach

/imagine prompt: https://s.mj.run/gGNA5xDRpCc In the midnight, the ocean beach, rippling light-blue waves, shining stars, weak lights, movie lights, HD, 4k, --ar 16:9 --no moon

宇航员月球种土豆:

astronaut planting potatoes on the moon

/imagine prompt: astronaut planting potatoes on the moon, nebula shines through glass dome, high detail, edge lighting, movie lighting, oc render, 8K --ar 5:3