C 为什么编译器生成的代码会在同一个内存位置反复写入相同的内容?
我使用C 为什么编译器生成的代码会在同一个内存位置反复写入相同的内容?,c,optimization,gcc,llvm-clang,C,Optimization,Gcc,Llvm Clang,我使用clang和gcc编译了以下代码,并使用-O3调用了这两个代码: #include <stdio.h> #include <stdlib.h> static void a(int n) { if (n == 0) return; printf("descending; a=%i\n", n); a(n-1); } int main() { a(5); return 0; } 这是《叮当声》中的一个: 080483d0 <main&g
clang
和gcc
编译了以下代码,并使用-O3
调用了这两个代码:
#include <stdio.h>
#include <stdlib.h>
static void a(int n) {
if (n == 0) return;
printf("descending; a=%i\n", n);
a(n-1);
}
int main() {
a(5);
return 0;
}
这是《叮当声》中的一个:
080483d0 <main>:
80483d0: 55 push %ebp
80483d1: 89 e5 mov %esp,%ebp
80483d3: 56 push %esi
80483d4: 83 ec 0c sub $0xc,%esp
80483d7: be 05 00 00 00 mov $0x5,%esi
80483dc: 8d 74 26 00 lea 0x0(%esi,%eiz,1),%esi
80483e0: 89 74 24 04 mov %esi,0x4(%esp)
80483e4: c7 04 24 e0 84 04 08 movl $0x80484e0,(%esp)
80483eb: e8 04 ff ff ff call 80482f4 <printf@plt>
80483f0: 4e dec %esi
80483f1: 75 ed jne 80483e0 <main+0x10>
80483f3: 31 c0 xor %eax,%eax
80483f5: 83 c4 0c add $0xc,%esp
80483f8: 5e pop %esi
80483f9: 5d pop %ebp
80483fa: c3 ret
主要原因是如何定义“堆栈帧”的所有权。简而言之,只要调用函数,该函数就会获得当前堆栈帧的所有权(包括参数、返回地址和任何局部变量) 它可以用它做任何事。无法保证
printf()
会保持堆栈框架中的任何内容完好无损。虽然这样做是不好的做法,但函数可以做到这一点,而且我编写的代码会弄乱堆栈(例如,在用C编写的C解释器中,它可以调用任何编译过的函数,尤其是printf()
)
因此,当printf()
返回时,无法保证指向静态字符串的指针仍在堆栈上。请注意,编译器不能假定printf()
将正常工作,因为您可以使用printf()
的自定义实现链接到不同的C运行时
尝试调用GCC可以看到源代码的函数。对于这些,它可能会进一步优化堆栈
[EDIT]也许另一个例子更容易理解。在某些情况下,GCC会优化函数参数传递。它们将被传递到CPU寄存器中,而不是将它们推送到堆栈上。让我们假设它使用
%eax
进行此操作。如果参数归调用函数所有,则不允许被调用函数修改%eax
简短的问题是函数调用。C编译器不能真正保证函数调用中会发生什么(除非它是C内置的,比如strlen
)。因此,它不必对它们进行优化。考虑:
pthread_mutex_lock(&mutex);
val = 5;
pthread_mutex_unlock(&mutex);
如果编译器推断对val的写入独立于对pthread\u mutex\u lock
的调用,那么它可能会在锁之前推送它,随后会出现大量混乱
编译器必须假定必须完成函数调用之前的最后一次写入。如果写入发生在一个代码块中,它确切地知道发生了什么(即,没有函数调用或只有内置),那么它可以随心所欲地进行优化,并拉出不需要的写入
编译器无法推断函数是否有权访问变量(至少在不解决停止问题的情况下是如此)。另一个函数可能在之前设置了地址,或者使用类似于
dlsym
的东西来计算它自己的二进制文件的地址,即使没有导出该符号。在Mac OS X应用程序二进制接口中,堆栈上传递的参数可能会被调用的例程修改。因此,如果调用例程将格式字符串的地址放在堆栈上,则被调用例程可以写入堆栈上的该位置。(通常不允许写入堆栈上更高[更早]的位置。)因此,调用例程无法知道,在被调用例程返回后,调用例程写入堆栈的参数保持不变
这对于在使用例程执行计算时可能修改其参数的例程来说是一种方便。例如,您可以有如下代码:
int foo(int x)
{
x *= x;
return x+3;
}
显然,对于这样简单的代码,编译器实际上不需要为了完成返回值的计算而将乘积存储在x中。但是,对于更复杂的例程,您可以看到编译器可能决定将值存储到x的位置
作为一个ABI设计问题,您可能想知道是否最好禁止被调用例程使用此空间,以便调用方可以依赖不改变的值。然而,以这种方式重复使用参数并不常见,而且编写新副本的成本也很低
还要注意,只有写入堆栈的格式字符串的地址可以更改。C语义要求被调用例程保持格式字符串的内容,因为它是const char。我猜这是因为允许函数修改堆栈中的值。例如,如果printf是
printf(char*c,inti){c=null;}
您的代码是用递归编写的。叮当声把它变成了一个循环;GCC展开它。这两个似乎都不像你希望的那样能起到很大的作用。@Gir:啊,听起来不错plausible@EricPostpischil:printf()
不允许修改指针指向的内存,但这不保护指针本身存储的内存区域。这不是由C定义的。汇编语言语义不由C标准解决。优化器可能无法选择是在寄存器中传递参数还是在堆栈中传递参数(静态函数除外)。参数传递由目标平台的应用程序二进制接口定义,外部可见函数必须符合该接口(以便可以从C以外的语言调用它们)。如果编译器对静态函数使用不同的ABI(外部不可见),那么寄存器是否属于被调用例程或调用例程取决于编译器,并且不以任何方式由C标准寻址。@EricPostSchil:除非被调用方是静态的或您正在执行LTO,对吗?@thejh:Yes,链接时间优化还允许选择在何处传递参数。@EricPostpischil:嗯,它是由ABI定义的。固定的。
pthread_mutex_lock(&mutex);
val = 5;
pthread_mutex_unlock(&mutex);
int foo(int x)
{
x *= x;
return x+3;
}