C程序中打印当前地址

12

假设我有以下简单的C程序:

int main() {

int a=5, b= 6, c;
c = a +b; 
return 0;
}

现在,我想知道表达式c=a+b的地址,也就是执行这个加法的程序地址。我能否使用printf来实现呢? 类似于下面这样的方式:

int main() {

int a=5, b= 6, c;
printf("Address of printf instruction in memory: %x", current_address_pointer_or_something)
c = a +b; 
return 0;
}

我知道可以使用gdb和info line file.c:line找到地址。但是,我想知道是否可以直接使用printf实现这个功能。


1
知道您使用的处理器架构和编译器将会很有帮助。共识似乎是没有真正可移植的方法来做到这一点。 - RichieHindle
你在查看的“指令”实际上可能被翻译成一系列跨越地址范围的汇编指令,这不是可能的吗? - user44511
8个回答

23

在gcc中,您可以使用&&运算符获取标签的地址。因此,您可以这样做:

int main() 
{
    int a=5, b= 6, c;

    sum:
        c = a+b;

    printf("Address of sum label in memory: %p", &&sum);
    return 0;
}

&&sum 的结果是,如果您执行 goto sum,将发出跳转指令的目标。因此,尽管在 C/C++ 中没有一对一的地址到行的映射,您仍然可以说“给我一个指向这段代码的指针”。


4
似乎这是一种非标准的扩展。 - 1800 INFORMATION
5
问题本身有各种非标准的方面。公平起见,回答应该包含非标准扩展。 - sigjuice
看起来很棒,我正在寻找类似于gcc编译器的东西!明天会试一下 ;) - Andrew
2
请注意,这可能与您想象的不同 - 例如,gcc可以决定a、b和c实际上是常量,将它们全部删除,然后sum有效地指向printf语句。 - bdonlan
是的,事实证明使用gcc -O1编译我的示例代码会完全优化掉a、b和c,因此打印的地址实际上与&main相同,并对应于printf的调用。 - Charlie
显示剩余2条评论

6

Visual C++中有一个称为_ReturnAddress的内置函数,可用于获取此处的一些信息。

例如:

__declspec(noinline) void PrintCurrentAddress()
{
    printf("%p", __ReturnAddress);
}

这将为您提供一个接近您所查看表达式的地址。但在某些优化(例如尾部折叠)的情况下,这可能不可靠。


2

在Visual Studio 2008中测试:

int addr;
__asm
{
    call _here
    _here: pop eax
    ; eax now holds the PC.
    mov [addr], eax
}

printf("%x\n", addr);

感谢这个问题的提出。


4
这样做会干扰CPU的返回地址预测器,因为你不是通过ret来修改返回地址。 - Michael
1
是不是直接放一个本地标签,然后使用立即MOV将该标签的地址加载到寄存器中会更好呢?例如:_here: mov eax, _here (newline) mov [addr], eax - bdonlan
@bdonlan 我认为使用这样的标签,你只能得到一个偏移量,而不是地址。 - phuclv

1

对于x86:

int test()
{
    __asm {
        mov eax, [esp]
    }
}


__declspec(noinline) int main() // or whatever noinline feature your compiler has
{
    int a = 5;
    int aftertest;

    aftertest = test()+3; // aftertest = disasms to 89 45 F8 mov dword ptr [a],eax.

    printf("%i", a+9);
    printf("%x", test());
    return 0;
}

1
这里有一种替代方法的草图:
假设你没有剥离调试符号,尤其是你有源级符号调试器需要实现单行单步、在源代码行设置断点等功能所需的行号到地址表。
大多数工具链使用相当好文档化的调试数据格式,并且通常会有帮助库实现大部分细节。
鉴于此并借助预处理器宏__LINE__(其求值为当前行号),应该可以编写一个查找任何源代码行地址的函数。
优点是不需要汇编语言,可通过调用特定于平台的调试信息库实现可移植性,并且不需要直接操作堆栈或使用打破 CPU 流水线的技巧。
一个很大的缺点是它比基于直接读取程序计数器的任何方法都要慢。

0

在i386或x86-64上使用gcc:

#include <stdio.h>

#define ADDRESS_HERE() ({ void *p; __asm__("1: mov 1b, %0" : "=r" (p)); p; })

int main(void) {
    printf("%p\n", ADDRESS_HERE());
    return 0;
}

请注意,由于编译器优化的存在,表达式的位置可能与原始源中的位置不对应。
使用这种方法的优点是,它不会改变函数的控制流图。它也不像使用 call 的方法那样破坏返回预测单元 :) 另一方面,它非常依赖于体系结构...而且因为它不扰乱 CFG,所以不能保证跳转到相关地址是有意义的。

你确定它不会改变CFG吗?据我理解,一个__asm__块就像一个标签一样,是一个调度器障碍。 - Charlie

0
如果编译器很好,这个加法操作会在寄存器中进行,而不是以你想象的方式存储在内存中。实际上,一个好的编译器会发现你的程序什么也没做,在函数内部操纵值但从未将这些值发送到函数外部,结果可能是没有代码。
如果你这样做:
c = a+b; printf("%u\n",c);
那么一个好的编译器也永远不会将该值C存储在内存中,它将保留在寄存器中,尽管这也取决于处理器。例如,如果针对该处理器的编译器使用堆栈将变量传递给函数,则会使用寄存器计算c的值(一个好的编译器会发现C始终为11并将其分配),并且该值将被放置在堆栈上,同时被发送到printf函数。自然地,由于其复杂性(无法将需要执行的所有操作都放入寄存器中),printf函数可能需要在内存中进行临时存储。
我的意思是说,你的问题没有答案。它非常依赖于处理器、编译器等等。没有通用的答案。我不得不想知道问题的根源是什么,如果你希望使用调试器进行探测,那么这不是要问的问题。
底线是,反汇编您的程序并查看它,对于那个特定的编译日期和设置,您将能够看到编译器放置中间值的位置。即使编译器为变量分配了内存位置,也不意味着程序将在该位置存储变量。这取决于优化。

0
我不知道细节,但应该有一种方法可以调用一个函数,然后爬取返回堆栈以获取调用者的地址,然后将其复制并打印出来。

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