如何在 C 语言中打印 EIP 地址?

3

这是我的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;
}

2
错误信息似乎表明您的汇编程序不认识“eip”作为可访问寄存器的名称。寄存器名称取决于体系结构,但GNU汇编程序不将“eip”列在i386的已识别寄存器名称列表中。 - John Bollinger
3
但它在ARM处理器上不存在,并且没有在C标准[n1570](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf)中提到。 - Basile Starynkevitch
4
为了以至少有点C语言的方式获得您当前代码的粗略eip/rip,您可以打印当前函数的地址(开头),例如printf("%p\n", &main); - Ped7g
3
我观察到 register int j asm("ebp"); 并不能保证变量 j 中一定包含 EBP 的值,除非它被用作扩展内联汇编模板的输入(或输入/输出)限制条件。这里它能正常工作更像是偶然。GCC 文档特别指出了这一点。 - Michael Petch
3
如果你想了解栈的工作原理,我建议你编写纯汇编代码(忘记内联汇编和_C_语言),然后使用GDB逐步执行并在执行指令时查看栈的值和内容将更加有用。问题在于C编译器可能会生成一堆难以通过打印值来追踪的改变栈上内容的指令(至少这是我的意见)。不同的优化级别也可能会改变生成的代码类型。如果研究栈,EIP寄存器的值并不是很有用。 - Michael Petch
显示剩余12条评论
3个回答

10

请先阅读QAReading program counter directly - 从中我们可以看到,没有mov命令可以直接访问EIP/RIP,因此您无法使用register asm来访问它。相反,在任何时候,您都可以使用这些技巧。在64位模式下最容易,使用

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

感谢Michael Petch指出在这里

演示:

#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时会进行校正)。


1
一种没有-7的方法是 asm volatile("1: lea 1b(%%rip), %0;": "=a"(rip)); - Michael Petch
1
@MichaelPetch 谢谢,已修复。这是我第一次使用RIP相对寻址。 - Antti Haapala -- Слава Україні
你根本不需要使用内联汇编;GNU C标签作为值扩展就可以解决问题。如果你确实想要使用内联汇编直接读取RIP,为什么不使用lea 0(%rip)而不是计算附近某个标签的地址呢?这样你就能看到RIP的真正工作方式了,即你会得到下一条指令的起始地址。 - Peter Cordes
@PeterCordes 我从未说过你需要使用inline,我是在问这个inline有什么问题。你看到我提供过任何回答吗?事实上,我最大的问题是所有的inline,包括OPs inline中与EBP和其他寄存器相关的问题。正如我在另一个答案下所说的“是的,inline有时可能看起来很简单,但是它是一个复杂的东西,在我看来只应作为最后的手段”。 - Michael Petch
@MichaelPetch:抱歉,我的标点符号不清楚。我在提到我的评论中的第二句话时向您发出了ping,因为我建议0(%rip)1b(%rip)更“有趣”。第一句话是一个单独的主题(我将其发布为答案),并且主要针对Antti和/或OP。 - Peter Cordes
显示剩余5条评论

1

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

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

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

如果您将其编译为PIC(位置无关代码),编译器会通过读取EIP或RIP来获取函数的运行时地址。 您不需要使用内联汇编。
除了函数地址外,GNU C 允许将标签作为值使用。
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.

}

使用 Godbolt编译器浏览器编译时加上和不加-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

无论您从标签中获取到有意义的地址与否,取决于编译器在放置标签的代码中进行了多少积极优化。将标签放置在某处可能会抑制优化(如自动向量化),即使您只是获取该标签地址,但我不确定。也许只有当您实际上使用goto时才会受到影响。

0
如果您感兴趣,可以使用另一种小技巧来读取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;
}

这是一个有趣的黑客技巧。你只需要调用下一条汇编指令。现在因为返回地址(在堆栈中的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作为数据的占位符。我个人喜欢这种方法,但正如评论中提到的那样,它可能会影响效率。

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

更新2:

请务必阅读有关红色区域的评论。非常感谢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));

1
我认为Antti Haapala的另一个答案更好,因为它仅依赖于使用RIP相对寻址和LEA指令来获取指令指针。虽然你的方法可行,但会导致返回地址预测器出现问题并且会影响性能。 - Michael Petch
3
如果这段汇编代码在Linux上是64位的话(我在用户输出中看到他好像在用Linux),那么当这个汇编模板被包含在一个叶函数内(不调用其他函数)并启用了优化时,就会存在一个微妙的问题,你可能会破坏红区内的数据。因此,在行内汇编开始时,你需要考虑从RSP中减去128,并在末尾将其还原(将128加回到RSP)。 - Michael Petch
1
“asm volatile”并不能防止所有优化,即使它能够做到这一点——它也不能防止编译器为红区生成代码。仅仅因为它在你的情况下给出了正确的输出,并不意味着它是正确的。内联汇编及其细微差别是使用它时应该谨慎的原因之一,而且你必须知道自己在做什么。我相信我可以生成一个测试用例来证明这种方法失败的可能性。 - Michael Petch
2
我有WSL(Windows Subsystem for Linux)。它是最新的,使用的是gcc version 4.8.4 (Ubuntu 4.8.4-2ubuntu1~14.04.3)。测试程序位于http://www.capp-sysware.com/misc/stackoverflow/redzone.c。除了我将`rip`变量放在全局范围内(这样会在更多环境中破坏红区),这是你的代码。我还创建了一个考虑到红区的版本。该程序应打印所有0的3个测试。使用`gcc test.c -O0 -mred-zone进行WSL测试时,您的测试打印1(而不是0)。如果我使用gcc test.c -O0 -mno-red-zone`,则所有测试都返回0。 - Michael Petch
1
@AnttiHaapala:是的,内联有时可能看起来很简单,但在我看来它是一个复杂的东西,应该只在最后一种情况下使用。 - Michael Petch
显示剩余7条评论

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