You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android平台创建可执行共享对象时的libc栈对齐崩溃问题及兼容性疑问

Troubleshooting Executable Shared Objects: Android vs. Linux Differences

Background & Implementation

You’ve created two files to build an executable shared object:

crt.c

const char service_interp[] __attribute__((section(".interp"))) = "${LD_SO}";
extern void _exit (int __status) __attribute__ ((__noreturn__));
int main();
void _start() { _exit(main()); }

Main.c

#include <jni.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
jint Java_Main_Main(JNIEnv*, jclass);
int main() {
    const char * str = "Hello from executable\n";
    write(1, str, strlen(str));
    return Java_Main_Main(NULL, NULL);
}
__attribute__((constructor)) void init() {
    printf("Hello from constructor\n");
}
__attribute__((destructor)) void fin() {
    printf("Hello from destructor");
}
jint Java_Main_Main(JNIEnv * env, jclass Main) {
    printf("Hello from shared object\n");
    return 0;
}

Compilation Setup

Using Clang 21.1.6 targeting x86_64-unknown-linux-android24, you replace the ${LD_SO} placeholder with the system linker path (retrieved via getldso.sh, which returns /system/bin/linker64) using these commands:

sed "s/\${LD_SO}/$(./getldso.sh | sed "s/\//\\\\//g")/g" crt.c | clang -x c - -c -fPIC -o crt.o
clang Main.c -g -fPIC -o libMain.so -DLD_SO=$(./getldso.sh) crt.o -shared

Observed Behavior

1. Loaded Execution (Java System.loadLibrary or C dlopen)

Output matches expectations:

Hello from constructor
Hello from shared object
Hello from destructor // Only printed when calling dlclose in C

2. Direct Execution

Running ./libMain.so causes a crash with this output:

Hello from constructor
Hello from executable
Segmentation fault

Debugging Findings

The crash occurs in the second printf call, at the assembly instruction movaps %xmm0,-0x30(%rbp). At this point, %rbp is 0x7fffffffdad8, so %rbp-0x30 is not 16-byte aligned—violating x86_64 System V ABI requirements and triggering a SIGSEGV.

Cross-Platform Comparison

On Linux (Clang 18.1.3 targeting x86_64-pc-linux-gnu), compiling the same code (adjusting linker path and JNI headers) results in successful direct execution with no crashes.


Answers to Your Questions

1. Is aligning %rsp in _start sufficient, or do we need to call __libc_init?

Aligning %rsp to a 16-byte boundary is not just a temporary workaround—it’s a non-negotiable requirement of the x86_64 System V ABI. All functions (including libc utilities like printf) assume the stack is 16-byte aligned when they’re invoked.

Your current _start function skips this critical step: when the kernel transfers control to _start, %rsp is 8-byte aligned (thanks to the return address pushed by the kernel). To fix the crash, adjust %rsp to meet the alignment requirement before calling main:

void _start() {
    // Align stack to 16 bytes: adjust if rsp is 8-byte aligned
    __asm__ volatile("andq $-16, %rsp");
    _exit(main());
}

As for __libc_init: Standard CRT (C runtime) code calls this to initialize libc’s internal state (heap setup, stdio initialization, signal handler configuration, etc.). When rolling your own _start, you may need to invoke it if you rely on complex libc features—but for basic I/O like printf, fixing stack alignment is often enough. That said, for full cross-platform compatibility (especially on Android), mimicking the standard CRT initialization flow is the safer approach.

2. Is this printf crash an Android bug, or implementation-defined behavior?

This is not an Android bug—it’s a result of bypassing standard CRT initialization and violating the x86_64 ABI. Linux’s glibc might be more forgiving in some cases (e.g., older versions handle unaligned stacks gracefully for certain functions), but this leniency isn’t guaranteed. Future glibc versions could tighten alignment checks, leading to identical crashes on Linux too.

Stack alignment in _start is a portable requirement, not an Android-specific quirk—you should always enforce it to comply with the ABI.

3. Why are constructor/destructor behaviors inconsistent across platforms and execution modes?

Let’s break down each scenario:

  • Constructor execution: Constructors are triggered by the dynamic linker when the shared object is loaded, which explains why they print in all loading scenarios. On Linux direct execution: some glibc versions skip constructor calls when a shared object is run as an executable (since it expects the standard CRT to handle initialization), while Android’s linker calls them regardless. This is an implementation detail of each platform’s dynamic linker.
  • Destructor execution: Destructors run when the shared object is unloaded (dlclose in C) or when the process exits (for loaded shared objects).
    • On Android direct execution: Your process crashes before reaching _exit, so destructors never get a chance to run.
    • On Linux direct execution: If the process exits cleanly, the linker should call destructors—but bypassing the standard CRT may prevent this trigger.
    • On Java loading (Linux vs Android): Linux’s JVM/dynamic linker ensures destructors are called when the library is unloaded, while Android’s ART (Android Runtime) may not handle this the same way—this could stem from differences in how ART manages shared library lifecycles.

These inconsistencies are implementation-defined behaviors—the C standard and dynamic linking specs don’t strictly mandate constructor/destructor timing for edge cases like executable shared objects.


内容的提问来源于stack exchange,提问作者Stas Badzi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.27 12:37:37