2014-10-28 112 views
1

在这个例子中的C程序,我打印出了什么地址?

int main (int argc, char* argv[]) { 
    printf("%p\n"); 
    return 0; 
} 

我很困惑,究竟我打印。每次运行程序时,它打印的地址都会发生变化,所以我认为地址与堆栈有关,比如可能从哪里开始或什么地方,但我不确定。

编辑:上面的程序来自Michael Howard和David LeBlanc(2003)编写的“编写安全代码”(第2版)的简单缓冲区溢出攻击的更详细示例。在foo方法中,第一个printf表示“我的堆栈看起来像:\ n%p ...等等,所以我想知道这是如何实现的,因为没有参数传递给printf函数,但我在这里问了,因为可能有这是我失踪了。我的道歉不包括其在原岗位。

#include <stdio.h> 
#include <string.h> 

void foo (const char* input) 
{ 
    char buf[10]; 

    printf("My stack looks like:\n%p\n%p\n%p\n%p\n%p\n%p\n\n"); 
    strcpy(buf, input); 
    printf("%s\n", buf); 
    printf("Now the stack looks like:\n%p\n%p\n%p\n%p\n%p\n%p\n\n"); 
} 

void bar (void) 
{ 
    printf("Augh! I've been hacked!\n"); 
} 

int main(int argc, char* argv[]) 
{ 
    printf("Address of foo = %p\n", foo); 
    printf("Address of bar = %p\n", bar); 
    if (argc != 2) { 
     printf("Please supply a string as an argument!\n"); 
       return -1; 
    } 

    foo (argv[1]); 
    return 0; 
} 
+2

@LeeDanielCrocker因为OP没有意识到它是错误的代码。这就是他/她提出这个问题的原因。 – Daniel 2014-10-28 20:28:43

回答

3

所以我们已经说明这是未定义的行为。您无法对调用未定义行为的代码进行任何 保证。因此,发布 的代码很可能不起作用,除非有一个非常具体的实现(例如,在x86上的 gcc 4.3,没有额外的编译器标志或优化,比方说)。

但是让我们玩得开心,猜码是如何打算用特定标志的特定编译器在特定的优化 水平上的特定 的工作平台。

主要的想法,这将帮助你在这里是编译器必须生成一些 代码给一个函数将其参数和功能必须有一些代码 才能够访问这些参数。然后有人(无论是调用者还是函数)必须有代码清除参数,以便内存(如果使用的话,根本不会泄露)。

但是,这里的问题是:编译器生成的代码调用函数可能不是 与编译该函数的编译器相同。

因此,平台架构师和编译器编写人员以及各种其他利益相关者 聚在一起并想出了一些calling conventions。调用 约定是平台的ABI的一部分,并且只要每个编译器 实现为相同的ABI,则它们的编译库将是兼容的。

由于ABI,我可以使用我的编译器实现一个函数,并为您生成目标文件 。您可以编写代码来调用此函数,并将 与我的目标文件(库)相链接。如果我们的编译器生成符合 的代码以符合正确的调用约定和ABI,那么它将全部解决。

正如你所猜测的,每个平台的调用约定是不同的。用于可变参数功能的 calling convention on x86 processorsprintf 称为cdecl调用约定。在这个调用约定中,调用者将所有参数压入堆栈(以相反顺序),一旦完成函数,调用者将参数弹出堆栈。

所以你看到的是你叫printf并提供了1个参数, 这是你的格式字符串。这是发生了什么(对于CDECL):

  1. 你的代码推的指针格式字符串压入堆栈,并调用 功能printf

  2. printf读取位于堆栈顶部的格式字符串。

  3. printf在格式字符串中看到%p。你已经告诉它,栈上有另外一个参数 ,这个参数是一个指针,而那个printf 应该打印这个指针的值。

  4. 但是堆栈上没有其他参数。 printf解释任何 任意垃圾作为指针在堆栈上并打印出来。

所以更多的%p的你给它,它会打印更多的堆栈数据。

它打印的垃圾是(你猜对了)undefined。你必须研究你的平台和编译器,以了解和理解那里的内容。

+0

谢谢,这个答案很棒! – Willwsharp 2014-10-29 22:03:03

4

该计划不确定的行为,因为在函数调用

printf("%p\n"); 

没有参数的格式说明%p从C标准

说明

2所述的函数fprintf写入输出到流指向 流,根据串的控制指向格式 指定后续参数如何被转换为输出。 如果 格式的参数不足,则行为是 未定义。

同样是有效的功能printf

+1

请注意,即使一个有效的指针被提供给'printf'函数,每次执行程序时(或者可能编译...这个地址可能仍然不同)...我已经考虑过C了,这已经有一段时间了) – Daniel 2014-10-28 20:28:09

+0

I编辑我原来的帖子,希望能增加更多的说明。 – Willwsharp 2014-10-28 20:50:28

1

你有一些undefined behavior(如answered by Vlad from Moscow)。另请参阅this。解释实际打印的值是试图理解未定义的行为,这需要了解具体的实现细节(什么ABI,生成了什么确切的机器代码 - 这取决于编译器,系统,处理器......,什么是机器状态在main开始时等等)。

为什么打印地址每次运行都不一样的原因是ASLR。编译器可能会在堆栈上传递一些地址,请参阅this & that。我想,运行时的行为也会如果你通过几种不同的参数,以您的main

+0

我编辑了我原来的帖子,希望能增加更多的说明。 – Willwsharp 2014-10-28 20:51:20

0

printf依靠格式字符串告诉(运行程序之前,在Linux上,用bash,与export SOME_LONG_VARIABLE_NAME=something-useless-but-long)有所不同,如果你改变你的环境它有多少种类的附加论据。当您在格式字符串中传递%p时,printf预计会有一个类型为void *的附加参数,并且它将尝试从任何需要附加参数(无论在堆栈中还是在寄存器中)读取值。这是否可以从恶意软件的角度来利用取决于该平台的调用约定。

如果传递给定转换说明符的错误类型的参数或参数太少,您可以从垃圾输出中获取任何内容到段错误。语言标准保留未定义的行为,这意味着编译器不需要发出警告或以任何特定方式处理问题。