gcc常用选项对代码的影响
by alert7
2001-12-21
测试环境 redhat 6.2
★ 前言
本文讨论gcc的一些常用编译选项对代码的影响。当然代码变了,它的内存布局也就会变了,随之exploit也就要做相应的变动。
gcc的编译选项实在太多,本文检了几个最常用的选项。
★ 演示程序
[alert7@redhat62 alert7]$ cat >; test.c
#include
void hi(void)
{
printf("hi"
}
int main(int argc, char *argv[])
{
hi();
return 0;
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
更多
发布评论
评论(9)
★ 一般情况
[alert7@redhat62 alert7]$ gcc -o test test.c
[alert7@redhat62 alert7]$ wc -c test
11773 test
[alert7@redhat62 alert7]$ gdb -q test
(gdb) disass main
Dump of assembler code for function main:
0x80483e4 : push %ebp
0x80483e5 : mov %esp,%ebp
0x80483e7 : call 0x80483d0
0x80483ec : xor %eax,%eax
0x80483ee : jmp 0x80483f0
0x80483f0 : leave
0x80483f1 : ret
....
End of assembler dump.
(gdb) disass hi
Dump of assembler code for function hi:
0x80483d0 : push %ebp
0x80483d1 : mov %esp,%ebp
0x80483d3 : push $0x8048450
0x80483d8 : call 0x8048308
0x80483dd : add $0x4,%esp
0x80483e0 : leave
0x80483e1 : ret
0x80483e2 : mov %esi,%esi
End of assembler dump.
来看看部分的内存映象
(内存高址)
+--------+
|bffffbc4| argv的地址(即argv[0]的地址)
0xbffffb84 +--------+
|00000001| argc的值
0xbffffb80 +--------+
|400309cb|main的返回地址
0xbffffb7c +--------+ <-- 调用main函数前的esp
|bffffb98| 调用main函数前的ebp
0xbffffb78 +--------+ <-- main函数的ebp
|080483ec| hi()的返回地址
0xbffffb74 +--------+
|bffffb78| 调用hi()前的esp
0xbffffb70 +--------+
|08048450| "hi"的地址
0xbffffb6c +--------+
| ...... |
(内存低址)
leave 指令所做的操作相当于MOV ESP,EBP 然后 POP EBP
ret 指令所做的操作相当于POP EIP
★ -O 编译选项
With `-O', the compiler tries to reduce code size and execution time.
When you specify `-O', the two options `-fthread-jumps' and
`-fdefer-pop' are turned on
优化,减少代码大小和执行的时间
[alert7@redhat62 alert7]$ gcc -O -o test test.c
[alert7@redhat62 alert7]$ wc -c test
11757 test
[alert7@redhat62 alert7]$ gdb -q test
(gdb) disass main
Dump of assembler code for function main:
0x80483d8 : push %ebp
0x80483d9 : mov %esp,%ebp
0x80483db : call 0x80483c8
0x80483e0 : xor %eax,%eax
0x80483e2 : leave
0x80483e3 : ret
0x80483e4 : nop
...
End of assembler dump.
(gdb) disass hi
Dump of assembler code for function hi:
0x80483c8 : push %ebp
0x80483c9 : mov %esp,%ebp
0x80483cb : push $0x8048440
0x80483d0 : call 0x8048308
0x80483d5 : leave
0x80483d6 : ret
0x80483d7 : nop
End of assembler dump.
在main()中,把一条jmp指令优化掉了,很显然,这条指令是可以不需要的。
在hi()中,把add $0x4,%esp优化掉了,这会不会使stack不平衡呢?
来看看部分的内存映象
(内存高址)
+--------+
|bffffbc4| argv的地址(即argv[0]的地址)
0xbffffb84 +--------+
|00000001| argc的值
0xbffffb80 +--------+
|400309cb|main的返回地址
0xbffffb7c +--------+ <-- 调用main函数前的esp
|bffffb98| 调用main函数前的ebp
0xbffffb78 +--------+ <-- main函数的ebp
|080483e0| hi()的返回地址
0xbffffb74 +--------+
|bffffb78| 调用hi()前的esp
0xbffffb70 +--------+
|08048440| "hi"的地址
0xbffffb6c +--------+
| ...... |
(内存低址)
leave指令所做的操作相当于把MOV ESP,EBP 然后 POP EBP。看到leave指令操作了没有,先把ebp-->;esp,再pop ebp,这样即使在过程内堆栈的esp,ebp是不平衡的,但只要返回时候碰到leave指令就会平衡了,所以把add $0x4,%esp优化掉也是没有问题的。
★ -O2 编译选项
-O2
Optimize even more. Nearly all supported optimizations that do
not involve a space-speed tradeoff are performed. Loop unrolling
and function inlining are not done, for example. As compared to -O,
this option increases both compilation time and the performance of
the generated code.
[alert7@redhat62 alert7]$ gcc -O2 -o test test.c
[alert7@redhat62 alert7]$ wc -c test
11757 test
[alert7@redhat62 alert7]$ gdb -q test
(gdb) disass main
Dump of assembler code for function main:
0x80483d8 : push %ebp
0x80483d9 : mov %esp,%ebp
0x80483db : call 0x80483c8
0x80483e0 : xor %eax,%eax
0x80483e2 : leave
0x80483e3 : ret
...
0x80483ef : nop
End of assembler dump.
(gdb) disass hi
Dump of assembler code for function hi:
0x80483c8 : push %ebp
0x80483c9 : mov %esp,%ebp
0x80483cb : push $0x8048440
0x80483d0 : call 0x8048308
0x80483d5 : leave
0x80483d6 : ret
0x80483d7 : nop
End of assembler dump.
由于程序比较简单,再优化也没有好优化的了,所以跟-O出来的一样。
★ -fomit-frame-pointer 编译选项
-fomit-frame-pointer
Don't keep the frame pointer in a register for functions
that don't need one. This avoids the instructions to save,
set up and restore frame pointers; it also makes an extra
register available in many functions. It also makes
debugging impossible on most machines.
忽略帧指针。这样在程序就不需要保存,安装,和恢复ebp了。这样ebp也就是一个free的register了,在函数中就可以随便使用了。
[alert7@redhat62 alert7]$ gcc -fomit-frame-pointer -o test test.c
[alert7@redhat62 alert7]$ wc -c test
11773 test
[alert7@redhat62 alert7]$ gdb -q test
(gdb) disass main
Dump of assembler code for function main:
0x80483e0 : call 0x80483d0
0x80483e5 : xor %eax,%eax
0x80483e7 : jmp 0x80483f0
0x80483e9 : lea 0x0(%esi,1),%esi
0x80483f0 : ret
....
End of assembler dump.
(gdb) disass hi
Dump of assembler code for function hi:
0x80483d0 : push $0x8048450
0x80483d5 : call 0x8048308
0x80483da : add $0x4,%esp
0x80483dd : ret
0x80483de : mov %esi,%esi
End of assembler dump.
在main()和hi()中都去掉了以下指令
push %ebp
mov %esp,%ebp//这两条指令安装
leave//这条指令恢复
来看看部分的内存映象
(内存高址)
+--------+
|bffffbc4| argv的地址(即argv[0]的地址)
0xbffffb84 +--------+
|00000001| argc的值
0xbffffb80 +--------+
|400309cb|main的返回地址
0xbffffb7c +--------+
|080483e5| hi()的返回地址
0xbffffb78 +--------+
|08048450| "hi"字符串的地址
0xbffffb74 +--------+
| ...... |
(内存低址)
没有保存上层执行环境的ebp.
★ -fomit-frame-pointer && -O2
-fomit-frame-pointer编译选项去掉了
push %ebp
mov %esp,%ebp//这两条指令安装
leave//这条指令恢复
-O2编译选项去掉了
add $0x4,%esp
两个加起来会不会这四条指令一起去掉,从而使stack不平衡呢?
[alert7@redhat62 alert7]$ gcc -fomit-frame-pointer -O2 -o test test.c
[alert7@redhat62 alert7]$ wc -c test
11741 test
[alert7@redhat62 alert7]$ gdb -q test
(gdb) disass main
Dump of assembler code for function main:
0x80483d8 : call 0x80483c8
0x80483dd : xor %eax,%eax
0x80483df : ret
End of assembler dump.
(gdb) disass hi
Dump of assembler code for function hi:
0x80483c8 : push $0x8048430
0x80483cd : call 0x8048308
0x80483d2 : add $0x4,%esp
0x80483d5 : ret
0x80483d6 : mov %esi,%esi
End of assembler dump.
来看看部分的内存映象
(内存高址)
+--------+
|bffffbc4| argv的地址(即argv[0]的地址)
0xbffffb84 +--------+
|00000001| argc的值
0xbffffb80 +--------+
|400309cb|main的返回地址
0xbffffb7c +--------+
|080483dd| hi()的返回地址
0xbffffb78 +--------+
|08048430| "hi"字符串的地址
0xbffffb74 +--------+
| ...... |
(内存低址)
此时就没有把add $0x4,%esp优化掉,如果优化掉的话,整个stack就
会变的不平衡,从而会导致程序出错。
★ -fPIC 编译选项
-fPIC If supported for the target machine, emit position-independent
code, suitable for dynamic linking,even if branches need large
displacements.
产生位置无关代码(PIC),一般创建共享库时用到。
在x86上,PIC的代码的符号引用都是通过ebx进行操作的。
[alert7@redhat62 alert7]$ gcc -fPIC -o test test.c
[alert7@redhat62 alert7]$ wc -c test
11805 test
[alert7@redhat62 alert7]$ gdb -q test
(gdb) disass main
Dump of assembler code for function main:
0x80483f8 : push %ebp
0x80483f9 : mov %esp,%ebp
0x80483fb : push %ebx
0x80483fc : call 0x8048401
0x8048401 : pop %ebx//取得该指令的地址
0x8048402 : add $0x1093,%ebx//此时ebx里面存放着是GOT表的地址
0x8048408 : call 0x80483d0
0x804840d : xor %eax,%eax
0x804840f : jmp 0x8048411
0x8048411 : mov 0xfffffffc(%ebp),%ebx
0x8048414 : leave
0x8048415 : ret
...
End of assembler dump.
(gdb) disass hi
Dump of assembler code for function hi:
0x80483d0 : push %ebp
0x80483d1 : mov %esp,%ebp
0x80483d3 : push %ebx
0x80483d4 : call 0x80483d9
0x80483d9 : pop %ebx
0x80483da : add $0x10bb,%ebx
0x80483e0 : lea 0xffffefdc(%ebx),%edx
0x80483e6 : mov %edx,%eax
0x80483e8 : push %eax
0x80483e9 : call 0x8048308
0x80483ee : add $0x4,%esp
0x80483f1 : mov 0xfffffffc(%ebp),%ebx
0x80483f4 : leave
0x80483f5 : ret
0x80483f6 : mov %esi,%esi
End of assembler dump.
来看看部分的内存映象
(内存高址)
+--------+
|bffffbc4| argv的地址(即argv[0]的地址)
0xbffffb84 +--------+
|00000001| argc的值
0xbffffb80 +--------+
|400309cb|main的返回地址
0xbffffb7c +--------+ <-- 调用main函数前的esp
|bffffb98| 调用main函数前的ebp
0xbffffb78 +--------+ <-- main函数的ebp
|401081ec| 保存的ebx
0xbffffb74 +--------+
|0804840d| (存放过call 0x8048401的下一条指令地址)
0xbffffb70 +--------+
|bffffb78| 调用hi()前的esp
0xbffffb6c +--------+
|08049494| GOT表地址
0xbffffb68 +--------+
|08048470|(存放过call 0x80483d9的下一条指令地址)
0xbffffb64 +--------+
| ...... |
(内存低址)
★ -static 编译选项
-static
On systems that support dynamic linking, this prevents
linking with the shared libraries. On other systems,
this option has no effect.
把一些函数都静态的编译到程序中,而无需动态链接了。
[alert7@redhat62 alert7]$ gcc -o test -static test.c
[alert7@redhat62 alert7]$ wc -c test
962808 test
[alert7@redhat62 alert7]$ gdb -q test
(gdb) disass main
Dump of assembler code for function main:
0x80481b4 : push %ebp
0x80481b5 : mov %esp,%ebp
0x80481b7 : call 0x80481a0
0x80481bc : xor %eax,%eax
0x80481be : jmp 0x80481c0
0x80481c0 : leave
0x80481c1 : ret
...
End of assembler dump.
(gdb) disass hi
Dump of assembler code for function hi:
0x80481a0 : push %ebp
0x80481a1 : mov %esp,%ebp
0x80481a3 : push $0x8071528
0x80481a8 : call 0x804865c
0x80481ad : add $0x4,%esp
0x80481b0 : leave
0x80481b1 : ret
0x80481b2 : mov %esi,%esi
End of assembler dump.
[alert7@redhat62 alert7]$ ldd test
not a dynamic executable
-static出来的代码已经没有PLT了,GOT虽然有,已经全部为0了。
一 基本语法
语法上主要有以下几个不同.
★ 寄存器命名原则
AT&T: %eax Intel: eax
★源/目的操作数顺序
AT&T: movl %eax,%ebx Intel: mov ebx,eax
★常数/立即数的格式
AT&T: movl $_value,%ebx Intel: mov eax,_value
把_value的地址放入eax寄存器
AT&T: movl $0xd00d,%ebx Intel: mov ebx,0xd00d
★ 操作数长度标识
AT&T: movw %ax,%bx Intel: mov bx,ax
★寻址方式
AT&T: immed32(basepointer,indexpointer,indexscale)
Intel: [basepointer + indexpointer*indexscale + imm32)
Linux工作于保护模式下,用的是32位线性地址,所以在计算地址时不用考虑segmentffset的问题.上式中的地址应为:
imm32 + basepointer + indexpointer*indexscale
下面是一些例子:
★直接寻址
AT&T: _booga ; _booga是一个全局的C变量
注意加上$是表示地址引用,不加是表示值引用.
注:对于局部变量,可以通过堆栈指针引用.
Intel: [_booga]
★寄存器间接寻址
AT&T: (%eax)
Intel: [eax]
★变址寻址
AT&T: _variable(%eax)
Intel: [eax + _variable]
AT&T: _array(,%eax,4)
Intel: [eax*4 + _array]
AT&T: _array(%ebx,%eax,
Intel: [ebx + eax*8 + _array]
二 基本的行内汇编
·基本的行内汇编很简单,一般是按照下面的格式:
asm("statements";
例如:asm("nop"; asm("cli";
·asm 和 __asm__是完全一样的.
·如果有多行汇编,则每一行都要加上 "\n\t"
例如:
asm( "pushl %eax\n\t"
"movl $0,%eax\n\t"
"popl %eax";
实际上gcc在处理汇编时,是要把asm(...)的内容"打印"到汇编文件中,所以格式控制字符是必要的.
再例如:
asm("movl %eax,%ebx";
asm("xorl %ebx,%edx";
asm("movl $0,_booga);
在上面的例子中,由于我们在行内汇编中改变了edx和ebx的值,但是由于gcc的特殊的处理方法,即先形成汇编文件,再交给GAS去汇编,所以GAS并不知道我们已经改变了edx和ebx的值,如果程序的上下文需要edx或ebx作暂存,这样就会引起严重的后果.对于变量_booga也存在一样的问题.为了解决这个问题,就要用到扩展的行内汇编语法.
三 扩展的行内汇编
扩展的行内汇编类似于Watcom.
基本的格式是:
asm ( "statements" : output_regs : input_regs : clobbered_regs);
clobbered_regs指的是被改变的寄存器.
下面是一个例子(为方便起见,我使用全局变量):
int count=1;
int value=1;
int buf[10];
void main()
{
asm(
"cld \n\t"
"rep \n\t"
"stosl"
:
: "c" (count), "a" (value) , "D" (buf[0])
: "%ecx","%edi" );
}
得到的主要汇编代码为:
movl count,%ecx
movl value,%eax
movl buf,%edi
#APP
cld
rep
stosl
#NO_APP
cld,rep,stos就不用多解释了.这几条语句的功能是向buf中写上count个value值.冒号后的语句指明输入,输出和被改变的寄存器.通过冒号以后的语句,编译器就知道你的指令需要和改变哪些寄存器,从而可以优化寄存器的分配.
其中符号"c"(count)指示要把count的值放入ecx寄存器
类似的还有:
a eax
b ebx
c ecx
d edx
S esi
D edi
I 常数值,(0 - 31)
q,r 动态分配的寄存器
g eax,ebx,ecx,edx或内存变量
A 把eax和edx合成一个64位的寄存器(use long longs)
我们也可以让gcc自己选择合适的寄存器.
如下面的例子:
asm("leal (%1,%1,4),%0"
: "=r" (x)
: "0" (x) );
这段代码实现5*x的快速乘法.
得到的主要汇编代码为:
movl x,%eax
#APP
leal (%eax,%eax,4),%eax
#NO_APP
movl %eax,x
几点说明:
1.使用q指示编译器从eax,ebx,ecx,edx分配寄存器.使用r指示编译器从eax,ebx,ecx,edx,esi,edi分配寄存器.
2.我们不必把编译器分配的寄存器放入改变的寄存器列表,因为寄存器已经记住了它们.
3."="是标示输出寄存器,必须这样用.
4.数字%n的用法:
数字表示的寄存器是按照出现和从左到右的顺序映射到用"r"或"q"请求的寄存器.如果我们要重用"r"或"q"请求的寄存器的话,就可以使用它们.
5.如果强制使用固定的寄存器的话,如不用%1,而用ebx,则asm("leal (%%ebx,%%ebx,4),%0"
: "=r" (x)
: "0" (x) );
注意要使用两个%,因为一个%的语法已经被%n用掉了.
下面可以来解释letter 4854-4855的问题:
1、变量加下划线和双下划线有什么特殊含义吗?
加下划线是指全局变量,但我的gcc中加不加都无所谓.
2、以上定义用如下调用时展开会是什么意思?
#define _syscall1(type,name,type1,arg1) \
type name(type1 arg1) \
{ \
long __res; \
/* __res应该是一个全局变量 */
__asm__ volatile ("int $0x80" \
/* volatile 的意思是不允许优化,使编译器严格按照你的汇编代码汇编*/
: "=a" (__res) \
/* 产生代码 movl %eax, __res */
: "0" (__NR_##name),"b" ((long)(arg1))); \
/* 如果我没记错的话,这里##指的是两次宏展开.
即用实际的系统调用名字代替"name",然后再把__NR_...展开.
接着把展开的常数放入eax,把arg1放入ebx */
if (__res >;= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}
本文提供了在 Linux 平台上使用和构造 x86 内联汇编的概括性介绍。他介绍了内联汇编及其各种用法的基础知识,提供了一些基本的内联汇编编码指导,并解释了在 Linux 内核中内联汇编代码的一些实例。
如果您是 Linux 内核的开发人员,您会发现自己经常要对与体系结构高度相关的功能进行编码或优化代码路径。您很可能是通过将汇编语言指令插入到 C 语句的中间(又称为内联汇编的一种方法)来执行这些任务的。让我们看一下 Linux 中内联汇编的特定用法。(我们将讨论限制在 IA32 汇编。)