【问题标题】:How to print EIP address in C? [duplicate]如何在C中打印EIP地址? [复制]
【发布时间】:2018-11-01 10:21:57
【问题描述】:

这是我的 C 程序...我试图打印出 ESP、EBP 和 EIP。

#include <stdio.h>
int main() {

    register int i asm("esp");
    printf("%#010x <= $ESP\n", i);

    int a = 1;
    int b = 2;
    char c[] = "A";
    char d[] = "B";

    printf("%p d = %s \n", &d, d);
    printf("%p c = %s \n", &c, c);
    printf("%p b = %d \n", &b, b);
    printf("%p a = %d \n", &a, a);

    register int j asm("ebp");
    printf("%#010x <= $EBP\n", j);

    //register int k asm("eip");
    //printf("%#010x <= $EIP\n", k);

    return 0;
}

ESP 和 EBP 没有问题。

user@linux:~# ./memoryAddress 
0xbffff650 <= $ESP
0xbffff654 d = B 
0xbffff656 c = A 
0xbffff658 b = 2 
0xbffff65c a = 1 
0xbffff668 <= $EBP
user@linux:~# 

但是当我尝试放置 EIP 代码时,编译时出现以下错误。

user@linux:~# gcc memoryAddress.c -o memoryAddress -g
memoryAddress.c: In function ‘main’:
memoryAddress.c:20:15: error: invalid register name for ‘k’
  register int k asm("eip");
               ^
user@linux:~#

这段代码有什么问题?

register int k asm("eip");
printf("%#010x <= $EIP\n", k);

是否可以通过C编程打印出EIP值?

如果是,请告诉我怎么做。

更新

我在这里测试过代码...

user@linux:~/c$ lscpu
Architecture:        i686
CPU op-mode(s):      32-bit
Byte Order:          Little Endian

感谢@Antti Haapala 和其他人的帮助。该代码有效......但是,当我将其加载到 GDB 中时,EIP 值是不同的。

(gdb) b 31
Breakpoint 1 at 0x68f: file eip.c, line 31.
(gdb) i r $eip $esp $ebp
The program has no registers now.
(gdb) r
Starting program: /home/user/c/a.out 
0x00000000 <= Low Memory Address
0x40055d   <= main() function
0x4005a5   <= $EIP 72 bytes from main() function (start)
0xbffff600 <= $ESP (Top of the Stack)
0xbffff600 d = B 
0xbffff602 c = A 
0xbffff604 b = 2 
0xbffff608 a = 1 
0xbffff618 <= $EBP (Bottom of the Stack)
0xffffffff <= High Memory Address

Breakpoint 1, main () at eip.c:31
31              return 0;
(gdb) i r $eip $esp $ebp
eip            0x40068f 0x40068f <main+306>
esp            0xbffff600       0xbffff600
ebp            0xbffff618       0xbffff618
(gdb) 

这是新代码

#include <stdio.h>
#include <inttypes.h>

int main() {

    register int i asm("esp");
    printf("0x00000000 <= Low Memory Address\n");
    printf("%p   <= main() function\n", &main);

    uint32_t eip;
    asm volatile("1: lea 1b, %0;": "=a"(eip));
    printf("0x%" PRIx32 "   <= $EIP %" PRIu32 " bytes from main() function (start)\n",
    eip, eip - (uint32_t)main);

    int a = 1;
    int b = 2;
    char c[] = "A";
    char d[] = "B";

    printf("%#010x <= $ESP (Top of the Stack)\n", i);

    printf("%p d = %s \n", &d, d);
    printf("%p c = %s \n", &c, c);
    printf("%p b = %d \n", &b, b);
    printf("%p a = %d \n", &a, a);

    register int j asm("ebp");
    printf("%#010x <= $EBP (Bottom of the Stack)\n", j);
    printf("0xffffffff <= High Memory Address\n");

    return 0;
}

【问题讨论】:

  • 错误消息似乎表明您的汇编程序无法将“eip”识别为可访问寄存器的名称。寄存器名称取决于体系结构,但 GNU 汇编器不会在 i386 的可识别寄存器名称中列出“eip”。
  • 但在ARM处理器上不存在,C标准中也没有提到n1570
  • 并且要以至少某种 C 方式粗略地了解当前代码的 eip/rip,您可以打印当前函数的地址(它的开头),例如 printf("%p\n", &amp;main);
  • 我将观察到 register int j asm("ebp"); 实际上并不能保证变量 j 将在其中具有 EBP 的值,除非它被用作输入(或输入/输出)约束到扩展的内联程序集模板。它在这里工作的事实或多或少是幸运的。 GCC 文档特别强调了这一点

标签: c linux assembly memory x86


【解决方案1】:

如果您有兴趣,可以阅读rip 和另一个小技巧。这里是你的完整代码,也就是rip

#include <stdio.h>
#include <inttypes.h>
int main()
{
    register uint64_t i asm("rsp");
    printf("%" PRIx64 " <= $RSP\n", i);

    int a = 1;
    int b = 2;
    char c[] = "A";
    char d[] = "B";

    printf("%p d = %s \n", &d, d);
    printf("%p c = %s \n", &c, c);
    printf("%p b = %d \n", &b, b);
    printf("%p a = %d \n", &a, a);

    register uint64_t j asm("rbp");
    printf("%" PRIx64 " <= $RBP\n", j);

    uint64_t rip = 0;

    asm volatile ("call here2\n\t"
                  "here2:\n\t"
                  "pop %0"
                  : "=m" (rip));
    printf("%" PRIx64 " <= $RIP\n", rip);

    return 0;
}

Hack 这里很有趣。你只需call 下一条装配线。现在因为堆栈中的返回地址为rip,您可以通过堆栈中的pop 指令检索它。 :)

更新:

这种方法的主要原因是数据注入。见以下代码:

#include <stdio.h>
#include <inttypes.h>
int main()
{
    uint64_t rip = 0;

    asm volatile ("call here2\n\t"
                  ".byte 0x41\n\t" // A
                  ".byte 0x42\n\t" // B
                  ".byte 0x43\n\t" // C
                  ".byte 0x0\n\t"  // \0
                  "here2:\n\t"
                  "pop %0"
                  : "=m" (rip));
    printf("%" PRIx64 " <= $RIP\n", rip);
    printf("injected data:%s\n", (char*)rip);

    return 0;
}

这种方法可以在代码段内注入数据(这对于代码注入很有用)。如果编译并运行,您会看到以下输出:

400542 <= $RIP
injected data:ABC

您已使用rip 作为数据的占位符。我个人喜欢这种方法,但它可能会产生 cmets 中提到的效率影响。

我已经在 Windows 的 64 位 Ubuntu bash(Windows 的 Linux 子系统)中测试了这两个代码,并且都可以工作。

更新 2:

请务必阅读有关 red zones 的 cmets。非常感谢michael 提到这个问题并提供了一个例子。 :) 如果您需要在没有红区问题的情况下使用此代码,则需要将其编写如下(来自 micheal 的示例):

asm volatile ("sub $128, %%rsp\n\t"
              "call 1f\n\t"
              ".byte 0x41\n\t" // A
              ".byte 0x42\n\t" // B
              ".byte 0x43\n\t" // C
              ".byte 0x0\n\t"  // \0
              "1:\n\t"
              "pop %0\n\t"
              "add $128, %%rsp"

              : "=r" (rip));

【讨论】:

  • 我认为 Antti Haapala 的另一个答案更好,因为它仅依赖于使用 RIP 相对寻址和使用 LEA 指令来获取指令指针。尽管您的方法有效,但它会导致返回地址预测器出现问题并造成性能损失。
  • 当然还有另一个微妙的问题如果这是 Linux 上的 64 位代码(我在用户输出中注意到他似乎在 Linux 上)。如果此程序集模板包含在叶函数中(不调用其他函数)并且已启用优化,则您将面临破坏 Red Zone 中数据的风险。您必须真正考虑在内联汇编开始时从 RSP 中减去 128,然后在最后恢复 RSP(将 128 添加到 RSP)。
  • asm volatile 不会阻止所有优化,但即使这样做 - 它也不会阻止编译器为红色区域生成代码。仅仅因为它在您的情况下给出的输出是正确的,并不意味着它是正确的。内联汇编及其细微差别是首选谨慎使用它的原因之一,然后您必须知道自己在做什么。我很确定我可以生成一个失败的测试用例
  • 我有 WSL(适用于 Linux 的 Windows 子系统)。它是最新的并且正在使用 gcc version 4.8.4 (Ubuntu 4.8.4-2ubuntu1~14.04.3) 。测试程序位于 capp-sysware.com/misc/stackoverflow/redzone.c 。除了我将rip 变量放在全局范围内(在红色区域打开的情况下会在更多环境中中断)之外,这是您的代码。我还创建了一个说明红色区域的版本。该程序应该为 3 个测试打印所有 0。使用 WSL 使用 gcc test.c -O0 -mred-zone 您的测试会打印 1 (not 0) 。如果我使用gcc test.c -O0 -mno-red-zone,所有测试都会返回 0。
  • @AnttiHaapala :是的,内联有时看起来很简单,但它是一个复杂的野兽,恕我直言,它只能作为最后的手段。
【解决方案2】:

EIP 无法直接读取。 RIP 可以,lea 0(%rip), %rax,但它不是通用寄存器。

您可以直接使用代码地址,而不是从寄存器中读取地址。

void print_own_address() {
    printf("%p\n", print_own_address);
}

如果您将其编译为 PIC(位置无关代码),编译器将通过读取 EIP 或 RIP 为您获取函数的运行时地址。 您不需要内联汇编。


或者对于函数以外的地址,GNU C allows labels as values

void print_label_address() {
    for (int i=0 ; i<1000; i++) {
        volatile int sink = i;
    }
  mylabel:
    for (int i=0 ; i<1000; i++) {
        volatile int sink2 = i;
    }
    printf("%p\n", &&mylabel);   // Take the label address with && GNU C syntax.

}

编译on the Godbolt compiler explorer 和不带-fPIE 以生成与位置无关的代码,我们得到:

  # PIE version:
    xor     eax, eax                   # i=0
.L4:                                   # do {
    mov     DWORD PTR -16[rsp], eax    #  sink=i
    add     eax, 1
    cmp     eax, 1000
    jne     .L4                        # } while(i!=1000);

    xor     eax, eax                   # i=0
.L5:                                   # do {
    mov     DWORD PTR -12[rsp], eax    # sink2 = i
    add     eax, 1
    cmp     eax, 1000
    jne     .L5                        # }while(i != 1000);

    lea     rsi, .L5[rip]           # address of .L5 = mylabel
    lea     rdi, .LC0[rip]          # format string
    xor     eax, eax                # 0 FP args in XMM regs for a variadic function
    jmp     printf@PLT              # tailcall printf

没有-fPIE,地址是链接时间常数(并且适合32位常数),所以我们得到

    mov     esi, OFFSET FLAT:.L5
    mov     edi, OFFSET FLAT:.LC0
    xor     eax, eax
    jmp     printf

您是否从标签中获得有意义的地址取决于编译器对您放置它的代码的优化程度。如果您甚至使用标签地址,但在 IDK.如果你真的有一个goto,也许只会伤害它。

【讨论】:

    【解决方案3】:

    请先阅读 QA Reading program counter directly - 从那里我们可以看到没有 mov 命令可以直接访问EIP/RIP,因此您不能使用register asm 访问给它。相反,您可以随时使用这些技巧。在 64 位模式下最简单,使用

    uint64_t rip;
    asm volatile("1: lea 1b(%%rip), %0;": "=a"(rip));
    

    获取 64 位指令(感谢 Michael Petch 在此处指出标签与 lea 一起使用。

    演示:

    #include <stdio.h>
    #include <inttypes.h>
    
    int main(void) {
        uint64_t rip;
       asm volatile("1: lea 1b(%%rip), %0;": "=a"(rip));
        printf("%" PRIx64 "; %" PRIu64 " bytes from main start\n",
               rip, rip - (uint64_t)main);
    }
    

    然后

    % gcc -m64 rip.c -o rip; ./rip
    55b7bf9e8659; 8 bytes from start of main
    

    证明它是正确的:

    % gdb -batch -ex 'file ./rip' -ex 'disassemble main'
    Dump of assembler code for function main:
       0x000000000000064a <+0>:     push   %rbp
       0x000000000000064b <+1>:     mov    %rsp,%rbp
       0x000000000000064e <+4>:     sub    $0x10,%rsp
       0x0000000000000652 <+8>:     lea    -0x7(%rip),%rax        # 0x652 <main+8>
    

    对于 32 位代码,您似乎可以使用带有标签的 lea - 不过这不适用于 64 位代码。

    #include <stdio.h>
    #include <inttypes.h>
    
    int main(void) {
        uint32_t eip;
        asm volatile("1: lea 1b, %0;": "=a"(eip));
        printf("%" PRIx32 "; %" PRIu32 " bytes from main start\n",
               eip, eip - (uint32_t)main);
    }
    

    然后

    % gcc -m32 eip.c -o eip; ./eip
    5663754a; 29 bytes from main start
    

    证明它是正确的:

    % gdb -batch -ex 'file ./eip' -ex 'disassemble main'  
    Dump of assembler code for function main:
       0x0000052d <+0>:     lea    0x4(%esp),%ecx
       0x00000531 <+4>:     and    $0xfffffff0,%esp
       0x00000534 <+7>:     pushl  -0x4(%ecx)
       0x00000537 <+10>:    push   %ebp
       0x00000538 <+11>:    mov    %esp,%ebp
       0x0000053a <+13>:    push   %ebx
       0x0000053b <+14>:    push   %ecx
       0x0000053c <+15>:    sub    $0x10,%esp
       0x0000053f <+18>:    call   0x529 <__x86.get_pc_thunk.dx>
       0x00000544 <+23>:    add    $0x1a94,%edx
       0x0000054a <+29>:    lea    0x54a,%eax
    

    (在32位版本中还有很多lea命令,但是这个是“在这里加载我的常量地址”,然后动态链接器在加载exe时会更正)。

    【讨论】:

    • 没有 -7 的方法是 asm volatile("1: lea 1b(%%rip), %0;": "=a"(rip));
    • @MichaelPetch 啊,谢谢,已修复。这是我第一次使用 RIP 相对寻址。
    • 你根本不需要内联汇编; the GNU C labels-as-values extension 成功了。 @MichaelPetch:如果您确实希望内联 asm 直接读取 RIP,为什么不使用 lea 0(%rip) 而不是计算附近标签的地址?然后你会看到 RIP 是如何工作的,即你得到了下一条指令的开始地址。
    • @PeterCordes 我从来没有说过你需要用内联来做,我是说这个内联有什么问题。您是否看到我提出这样的答案?事实上,我最大的问题是所有内联问题,包括 OP 内联问题,甚至涉及 EBP 和其他寄存器等问题。正如我在另一个答案中所说的那样是的,内联有时看起来很简单,但它是一种复杂的野兽,只能作为最后的手段,恕我直言
    • @MichaelPetch:抱歉,我的标点符号不清楚。我在回复你:我评论中的第二句话,因为我建议0(%rip)1b(%rip) 更“有趣”。第一句话是一个单独的主题(我将其作为答案发布),主要针对 Antti 和/或 OP。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-08-01
    • 1970-01-01
    • 2015-12-31
    • 2019-09-02
    • 1970-01-01
    • 1970-01-01
    • 2012-06-22
    相关资源
    最近更新 更多