C++跳转到其他方法执行

14

在我的C++ JNI-Agent项目中,我正在实现一个函数,该函数将接收可变数量的参数,并将执行传递给另一个函数:

// address of theOriginalFunction
public static void* originalfunc;

void* interceptor(JNIEnv *env, jclass clazz, ...){

    // add 4 to the function address to skip "push ebp / mov ebp esp"
    asm volatile("jmp *%0;"::"r" (originalfunc+4));

    // will not get here anyway
    return NULL;
}

上述函数只需跳转到:

JNIEXPORT void JNICALL Java_main_Main_theOriginalFunction(JNIEnv *env, jclass clazz, jboolean p1, jbyte p2, jshort p3, jint p4, jlong p5, jfloat p6, jdouble p7, jintArray p8, jbyteArray p9){
    // Do something
}

上述代码完美运行,原始函数可以正确读取所有参数(已测试包括数组等不同类型的9个参数)。

然而,在从拦截器跳转到原始函数之前,我需要进行一些计算。但是,这里我观察到了有趣的行为。

void* interceptor(JNIEnv *env, jclass clazz, ...){
    int x = 10;
    int y = 20;
    int summ = x + y;

    // NEED TO RESTORE ESP TO EBP SO THAT ORIGINAL FUNCTION READS PARAMETERS CORRECTLY
    asm (
        "movl %ebp, %esp;"
        "mov %rbp, %rsp"
    );

    // add 4 to the function address to skip "push ebp / mov ebp esp"
    asm volatile("jmp *%0;"::"r" (originalfunc+4));

    // will not get here anyway
    return NULL;
}

这仍然有效,我能够进行一些基本的计算,然后重置堆栈指针并跳转到我的原始函数,原始函数也正确读取var_args中的参数。但是:如果我用mallocprintf(“任何字符串”)替换基本的int操作,那么,不知何故,如果跳转到我的原始函数,那么我的参数就会混乱,并且原始函数最终读取错误的值...

我已经尝试调试此行为并检查内存区域以查看出了什么问题...在跳转之前,那里的一切看起来都很好,ebp后面跟着函数参数。

如果没有复杂的计算就进行跳转,则一切都正常工作,ebp后面的内存区域不会更改。原始函数读取正确的值...

现在,如果我在执行printf(例如)后进行跳转,原始方法读取的参数将被破坏...

是什么导致了这种奇怪的行为?printf甚至不在我的方法中存储任何局部变量...好吧,它确实在寄存器中存储一些文字常量,但是为什么我的堆栈只有在跳转之后才会被破坏,而不是在之前就已经破坏了呢?

我在这个项目中使用的是运行在Windows机器上的g++ 4.9.1编译器。

是的,我关心std::forward和模板选项,但它们在我的情况下不起作用...还有,我知道跳转到其他方法有点hacky,但这是我使JNI-interceptor工作的唯一想法...

******************** 编辑 ********************

如讨论所述,我正在添加带有源函数的生成汇编代码。

没有printf的函数(正常工作):

void* interceptor(JNIEnv *env, jclass clazz, ...){

    //just an example
    int x=8;

    // restoring stack pointers
    asm (
        "movl %ebp, %esp;"
        "mov %rbp, %rsp"
    );

    // add 4 to the function address to skip "push ebp / mov ebp esp"
    asm volatile("jmp *%0;"::"r" (originalfunc+4));

    // will not get here anyway
    return NULL;
}

void* interceptor(JNIEnv *env, jclass clazz, ...){
    // first when interceptor is called, probably some parameter restoring...
    push %rbp
    mov %rsp %rbp
    sub $0x30, %rsp
    mov %rcx, 0x10(%rbp)
    mov %r8, 0x20(%rbp)
    mov %r9, 0x28(%rbp)
    mov %rdx, 0x18(%rbp)

    // int x = 8;
    movl $0x8, -0x4(%rbp)

    // my inline asm restoring stack pointers
    mov %ebp, %esp
    mov %rbp, %rsp

    // asm volatile("jmp *%0;"::"r" (originalfunc+4))
    mov 0xa698b(%rip),%rax      // store originalfunc in rax
    add %0x4, %rax
    jmpq *%rax

    // return NULL;
    mov $0x0, %eax
}

现在是printf变体的汇编输出...

void* interceptor(JNIEnv *env, jclass clazz, ...){

    //just an example
    int x=8;

    printf("hey");

    // restoring stack pointers
    asm (
        "movl %ebp, %esp;"
        "mov %rbp, %rsp"
    );

    // add 4 to the function address to skip "push ebp / mov ebp esp"
    asm volatile("jmp *%0;"::"r" (originalfunc+4));

    // will not get here anyway
    return NULL;
}

void* interceptor(JNIEnv *env, jclass clazz, ...){
    // first when interceptor is called, probably some parameter restoring...
    push %rbp
    mov %rsp %rbp
    sub $0x30, %rsp
    mov %rcx, 0x10(%rbp)
    mov %r8, 0x20(%rbp)
    mov %r9, 0x28(%rbp)
    mov %rdx, 0x18(%rbp)

    // int x = 8;
    movl $0x8, -0x4(%rbp)

    // printf("hey");
    lea 0x86970(%rip), %rcx   // stores "hey" in rcx???
    callq 0x6b701450          // calls the print function, i guess

    // my inline asm restoring stack pointers
    mov %ebp, %esp
    mov %rbp, %rsp

    // asm volatile("jmp *%0;"::"r" (originalfunc+4))
    mov 0xa698b(%rip),%rax      // store originalfunc in rax
    add %0x4, %rax
    jmpq *%rax

    // return NULL;
    mov $0x0, %eax
}

这里是 printf 函数的汇编代码:

printf(char const*, ...)
    push %rbp
    push %rbx
    sub $0x38, %rsp
    lea 0x80(%rsp), %rbp
    mov %rdx, -0x28(%rbp)
    mov $r8, -0x20(%rbp)
    mov $r9, -0x18(%rbp)
    mov $rcx, -0x30(%rbp)
    lea -0x28(%rbp), %rax
    mov %rax, -0x58(%rbp)
    mov -0x58(%rbp), %rax
    mov %rax, %rdx
    mov -0x30(%rbp), %rcx
    callq 0x6b70ff60 // (__mingw_vprintf)
    mov %eax, %ebx
    mov %ebx, %eax 
    add $0x38, %rsp
    pop %rbx
    pop %rbp
    retq

看起来 printf 在 rbp 上执行了许多操作,但我没有发现任何问题...

这是被拦截函数的汇编代码。

push %rbp              // 1 byte
push %rsp, %rbp        // 3 bytes , need to skip them
sub $0x50, %rsp
mov %rcx, 0x10(%rbp)
mov %rdx, 0x18(%rbp)
mov %r8d, %ecx
mov %r9d, %edx
mov 0x30(%rbp), %eax
mov %cl, 0x20(%rbp)
mov %dl, 0x28(%rbp)
mov %ax, -0x24(%rbp)

************* EDIT 2 **************

我认为了解运行时内存如何变化很有用:

第一张图片显示进入拦截器函数后的内存布局:

Memory Layout when entering the interceptor

第二张图片显示了同一内存区域在出现问题的代码(例如 printf 等)之后的情况。

enter image description here

第三张图片显示跳转到原始函数后的内存布局。

enter image description here

可以看到,在调用 printf 后,堆栈看起来很正常,但是当我跳转到原始函数时,它变得混乱了......

从这些截图中可以看出,所有参数都位于内存中的堆栈上,并且参数不是通过寄存器传递的。


1
只是一点评论:在我看来,push ebp / mov ebp esp 是3个字节长,而不是4个。 - manuell
我用你的代码制作了一个小型的C独立程序,但没有使用JNI。它非常好用,在跳转之前使用printf和malloc。但是它是在Windows、Visual Studio 2010、32位下运行的。你应该仔细检查你的代码,并在你的问题中添加更多的细节,包括调试时生成/查看的汇编信息。 - manuell
4
请查看您的编译器文档以了解ABI。通常,某些参数是通过寄存器传递的,而printf(或其他函数)可能不会恢复它们。 - rAndom69
谢谢大家的回复,我已经将汇编代码添加到初始帖子中... @chtz 我想跳过push/mov部分,以便var_args参数能够被我的拦截函数正确读取...你提到了在跳转之前反转堆栈修改。我该怎么做?这样做对这种情况有帮助吗? - Aksim Elnik
1
@AksimElnik 一张地图?模板不起作用?好的,那么我在另一个讨论中为您准备了一些东西... - Aconcagua
显示剩余12条评论
3个回答

1

使用一组调用约定,在汇编中手动传递参数。在这种情况下,参数从%rcx寄存器开始传递。对作为调用约定使用的寄存器进行任何修改都会改变任何后续jmp所感知到的参数。

jmp之前调用printf将把%rcx的值从*env更改为指向常量"hello"的指针。在更改%rcx的值后,您需要将其恢复为先前的值。以下代码应该可以工作:

void* interceptor(JNIEnv *env, jclass clazz, ...){

//just an example
int x=8;

printf("hey");

// restoring stack pointers
asm (
    "movl %ebp, %esp;"
    "mov %rbp, %rsp"
);

// restore %rcx to equal *env
asm volatile("mov %rcx, 0x10(%rbp)");

// add 4 to the function address to skip "push ebp / mov ebp esp"
asm volatile("jmp *%0;"::"r" (originalfunc+4));

// will not get here anyway
return NULL;

}


嗨,你确实是对的,我需要恢复一些寄存器...但是你的代码没有恢复rcx,而是将rcx放入了0x10(%rbp):D 不过没关系,你给了我一个好主意,导致问题得到了解决。 - Aksim Elnik
1
我很感激您的反馈。我编写的代码遵循您发布的汇编中所见到的规则,即左侧的值/寄存器是给定指令的目标。因此,该代码实际上将 %rbp + 10 放入 %rcx 寄存器中,但汇编语言真的很令人困惑,所以呃呃呃。 - Nash

0

很可能在转发之前调用的任何函数都会破坏处理可变参数列表所需的结构(在您的汇编代码中仍然存在mingw_printf调用,您没有显示其反汇编)。

为了更好地理解发生了什么,您可能需要查看this question

要解决您的问题,您可以考虑添加另一个间接引用,我认为以下内容可能有效(但我尚未测试过)。

void *forward_interceptor(env, clazz, ... ) {
    // add 4 to the function address to skip "push ebp / mov ebp esp"
    asm volatile("jmp *%0;"::"r" (originalfunc+4));
    // will not get here anyway
    return NULL;
}

void* interceptor(JNIEnv *env, jclass clazz, ...){
    //do your preparations 
    ...

    va_list args;
    va_start(args, clazz);
    forward_interceptor(env, clazz, args);
    va_end(args);
}

在我看来,重要的是你需要设置va_list/va_start/va_end来确保参数正确地传递到下一个函数。

然而,既然你似乎知道你要转发的函数的签名,并且它似乎不接受可变数量的参数,为什么不提取参数并像这样正确地调用函数:

void* interceptor(JNIEnv *env, jclass clazz, ...){
    //do your preparations 
    ...

    va_list args;
    va_start(args, clazz);

    jboolean p1 = va_arg(args, jboolean); 
    jbyte p2 =  va_arg(args, jbyte); 
    jshort p3 = va_arg(args, jshort); 
    ...
    Java_main_Main_theOriginalFunction(env, clazz, p1, p2, ...
    va_end(args);

    return NULL; 
}

请注意,va_arg无法检查参数是否为正确的类型或是否可用。

传递 va_list 不会再次推送所有可变数量的参数! - Ben Voigt
@BenVoigt 能否详细说明你的意思? - Gert Wollny
@GertWollny forward_interceptor(env, clazz, args); 你将va_list传递给了forward_interceptor函数,但这并不等同于将interceptor的所有参数传递给forward_interceptor。相反,在forward_interceptor中应用va_start后,你只能在那里获取(外部)va_list参数(因此,在forward_interceptor中会有两个这样的列表,一个是从interceptor传递过来的,另一个是需要检索前者的自己的列表...)。 - Aconcagua

0
这是什么架构?从寄存器名称来看,它似乎是x64。
你说参数不对。我同意。但你就认为栈也错了,可能不是这样的。x64在寄存器中传递一些参数,但不包括可变参数。因此,你的转发器函数签名与你尝试调用的函数不兼容。
发布一个直接调用Java_main_Main_theOriginalFunction的汇编代码,然后再发布一个使用完全相同参数调用你的转发器的代码;你会看到参数传递方式的巨大差异。

我非常确定参数是通过堆栈传递的...我在我的初始帖子中添加了一些图片,展示了这一点。然而,printf确实会修改一些寄存器,影响我的原始函数的工作流程...但这部分对我来说不太清楚。是否可能将参数的内存块复制到堆栈上,并调用我的原始函数? - Aksim Elnik
你在堆栈上看到参数,是因为你调用了一个可变参数函数interceptor,它使用可变参数调用约定。然而,原始函数不是可变参数的,很可能期望使用fastcall调用约定,该约定将参数传递给寄存器。也许不是这样,这取决于这些JNIEXPORTJNICALL宏中隐藏了什么。如果这些中指定了调用约定,那么你确实需要在interceptor上应用它。 - Ben Voigt
@AksimElnik:你强迫我猜测你的架构,这绝对没有帮助。 - Ben Voigt
2
@AksimElnik:如果是x86_64,你需要阅读这篇文章:http://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64 你假设所有参数都在堆栈上是错误的,并且你没有考虑到RED ZONE。 - Ben Voigt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接