14: 55 push %ebp
15: 89 e5 mov %esp,%ebp
17: 83 ec 18 sub $0x18,%esp
1a: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%ebp)
21: c7 45 fc 02 00 00 00 movl $0x2,-0x4(%ebp)
28: c7 44 24 04 02 00 00 movl $0x2,0x4(%esp)
2f: 00
30: c7 04 24 01 00 00 00 movl $0x1,(%esp)
37: e8 fc ff ff ff call 38
3c: b8 00 00 00 00 mov $0x0,%eax
41: c9 leave
42: c3 ret
1.%ebp---是帧寄存器,在函数中就是函数的基址寄存器,指向一个函数的栈底(帧底)。
2.%esp---是栈寄存器,相当于是整个程序的基址寄存器,始终指向栈顶。
3.push %ebp 的意思是%ebp入栈,此时的%ebp保存的是上一个函数的帧起始地址,也即调用该函数的地址。 把%ebp压栈,保存起来,以便返回。
4.mov %esp,%ebp 的意思是 把%esp赋值给%ebp,%esp保存的是当前程序的栈顶,也即该函数所占用内存的起始地址。 把%esp赋值给%ebp,也就把%ebp设置成了当前函数的帧起始地址。
5.sub $***,%esp,并不会在每个程序中都会出现。可以尝试一下,如果一个函数没有任何局部变量,那么反汇编这句话也就 不会出来。这句话的意思是,把%esp减去一个数。我们知道栈空间是由高到底发展的,所以%esp++,相当于%esp=%esp-1。因为调用了新函数,而且该函数有局部变量,那么栈空间就变大了,所以要扩展栈空间,也即是修改%esp,让其指向更低的地址。而让%esp减去多少呢?这要看函数占用多少空间,于其中的局部变量有关,以及他将调用的函数参数有关。其并不计算其参数所占的空间,其参数所占的空间要算在调用它的函数中。
6.call指令会将函数返回地址(call指令的下一条指令地址)入栈,并自动调整esp值,而ret指令则将栈顶值出栈,并作为函数返回地址,即装入eip,也自动调整esp,而leave指令,则相当于move esp,ebp;pop ebp;所以函数执行完毕之后,栈顶元素正确的指向调用函数之前压栈的eip函数返回地址。还有一点需要注意的是,call指令压栈的返回地址占用的空间大小。从被调用函数sum中对其参数的引用地址来看,貌似压入的是8个字节,但是感觉不合理,因为eip是4个字节啊,思考很久,才明白,是因为sum函数一开始进行了push ebp,保存了父函数的栈底地址,就这样又占用了4个字节,加上返回地址的4个字节,共8个字节。
关于第5点,还需要详细说明一下,我做了好多实验,最终发现,sub $***,%esp中,esp减去的数字,即扩展的栈空间的大小,等于本函数内部的有效局部变量的大小加上被调用函数的参数所占空间大小。具体来说,对于本函数内部的局部变量,栈空间的大小以16个字节为增长单位,即如果函数内的有效局部变量占用的空间不大于16个字节,就按照16个字节算,比如,若只有两个int变量,且一个int占4个字节,则为其分配16个字节,即sub $0x10,%esp;若有4个int,同样是sub $0x10,%esp;但若有5个int,或者4个int,一个char(一个char占一个字节),则sub $0x20,%esp;而对于函数调用来说,就不一样了。函数调用的参数占用的栈空间以4字节为单位分配,即一个int就分配4个字节,两个int就分配8个字节,而一个char也分配4个字节(不足为奇,因为硬件就是以4个字节为单位压栈出栈的)。注意,对于本函数局部参数而言,两个int,三个char仍然占用16个字节。注意,上面所说的有效局部变量,是指在函数内被赋值的变量,若没赋值也没被使用,则不分配栈空间。
但是,我发现一个奇怪的现象,即对于被调用函数的参数,若存在char类型的参数,则其栈空间。。。看如下代码:
#include
int sum(int a,char b,char c)
{
//a = 7;
//b = 5;
return 0;
}
int main(void)
{
int a,b;
a = 1;
b = 2;
sum(1,2,3);
return 0;
}
反汇编:
b.o: 文件格式 elf32-i386
Disassembly of section .text:
00000000
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 08 sub $0x8,%esp
6: 8b 55 0c mov 0xc(%ebp),%edx
9: 8b 45 10 mov 0x10(%ebp),%eax
c: 88 55 fc mov %dl,-0x4(%ebp)
f: 88 45 f8 mov %al,-0x8(%ebp)
12: b8 00 00 00 00 mov $0x0,%eax
17: c9 leave
18: c3 ret
00000019
19: 55 push %ebp
1a: 89 e5 mov %esp,%ebp
1c: 83 ec 1c sub $0x1c,%esp
1f: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%ebp)
26: c7 45 fc 02 00 00 00 movl $0x2,-0x4(%ebp)
2d: c7 44 24 08 03 00 00 movl $0x3,0x8(%esp)
34: 00
35: c7 44 24 04 02 00 00 movl $0x2,0x4(%esp)
3c: 00
3d: c7 04 24 01 00 00 00 movl $0x1,(%esp)
44: e8 fc ff ff ff call 45
49: b8 00 00 00 00 mov $0x0,%eax
4e: c9 leave
4f: c3 ret
可见,在sum函数的汇编代码中,sub $0x8,%esp,即又为两个char类型的参数分配了栈空间,并从其父函数(调用sum函数的函数main)的栈空间中将其值的低两个字节复制过来。why?另外,我注意到,若在sum函数中直接引用其参数a,b,int 类型参数a的重新赋值会直接引用其父函数中的栈空间,但char类型参数b不会直接引用其父函数栈中对应的参数空间,而是在sum函数中为其重新分配栈空间,why?如下:
#include
int sum(int a,char b,char c)
{
a = 7;
b = 5;
return 0;
}
int main(void)
{
int a,b;
a = 1;
b = 2;
sum(1,2,3);
return 0;
}
反汇编:
b.o: 文件格式 elf32-i386
Disassembly of section .text:
00000000
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 18 sub $0x18,%esp
6: 8b 55 0c mov 0xc(%ebp),%edx
9: 8b 45 10 mov 0x10(%ebp),%eax
c: 88 55 ec mov %dl,-0x14(%ebp)
f: 88 45 e8 mov %al,-0x18(%ebp)
12: c7 45 fc 07 00 00 00 movl $0x7,-0x4(%ebp)
19: c6 45 fb 05 movb $0x5,-0x5(%ebp)
1d: b8 00 00 00 00 mov $0x0,%eax
22: c9 leave
23: c3 ret
00000024
24: 55 push %ebp
25: 89 e5 mov %esp,%ebp
27: 83 ec 1c sub $0x1c,%esp
2a: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%ebp)
31: c7 45 fc 02 00 00 00 movl $0x2,-0x4(%ebp)
38: c7 44 24 08 03 00 00 movl $0x3,0x8(%esp)
3f: 00
40: c7 44 24 04 02 00 00 movl $0x2,0x4(%esp)
47: 00
48: c7 04 24 01 00 00 00 movl $0x1,(%esp)
4f: e8 fc ff ff ff call 50
54: b8 00 00 00 00 mov $0x0,%eax
59: c9 leave
5a: c3 ret
从反汇编代码中可见, sub $0x18,%esp/在sum中为a,b按照本地函数局部变量栈空间分配原则分配了16字节的栈空间,加上为两个char类型的参数所分配的栈空间,总共就是0x18,即24字节。
但是随后的实验证明,如果在sum声明并定义局部变量而导致分配16字节的栈空间,则对int类型参数a的引用会直接引用父函数中的栈空间。
C代码:
#include
int sum(int a,char b,char c)
{
int s = a;
a = 7;
// b = 5;
return 0;
}
int main(void)
{
int a,b;
a = 1;
b = 2;
sum(1,2,3);
return 0;
}
反汇编:
b.o: 文件格式 elf32-i386
Disassembly of section .text:
00000000
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 18 sub $0x18,%esp
6: 8b 55 0c mov 0xc(%ebp),%edx
9: 8b 45 10 mov 0x10(%ebp),%eax
c: 88 55 ec mov %dl,-0x14(%ebp)
f: 88 45 e8 mov %al,-0x18(%ebp)
12: 8b 45 08 mov 0x8(%ebp),%eax
15: 89 45 fc mov %eax,-0x4(%ebp)
18: c7 45 08 07 00 00 00 movl $0x7,0x8(%ebp)
1f: b8 00 00 00 00 mov $0x0,%eax
24: c9 leave
25: c3 ret
00000026
26: 55 push %ebp
27: 89 e5 mov %esp,%ebp
29: 83 ec 1c sub $0x1c,%esp
2c: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%ebp)
33: c7 45 fc 02 00 00 00 movl $0x2,-0x4(%ebp)
3a: c7 44 24 08 03 00 00 movl $0x3,0x8(%esp)
41: 00
42: c7 44 24 04 02 00 00 movl $0x2,0x4(%esp)
49: 00
4a: c7 04 24 01 00 00 00 movl $0x1,(%esp)
51: e8 fc ff ff ff call 52
56: b8 00 00 00 00 mov $0x0,%eax
5b: c9 leave
5c: c3 ret
但对char类型参数的引用仍会引用在本地函数内分配的新栈空间。why?
最后要说明的一点是,对C语言中是要将for循环中的局部变量放循环内部声明还是放循环外部效率高的问题,见如下C代码和反汇编:
C:
#include
int sum(int a,char b,char c)
{
// int s = a;
// a = 7;
// b = 5;
return 0;
}
int main(void)
{
int a,b;
a = 1;
b = 2;
//sum(1,2,3);
for (a=0;a<9;a++)
{
int x = 4;
}
int y = 5,yy=6;
return 0;
}
反汇编:
b.o: 文件格式 elf32-i386
Disassembly of section .text:
00000000
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 08 sub $0x8,%esp
6: 8b 55 0c mov 0xc(%ebp),%edx
9: 8b 45 10 mov 0x10(%ebp),%eax
c: 88 55 fc mov %dl,-0x4(%ebp)
f: 88 45 f8 mov %al,-0x8(%ebp)
12: b8 00 00 00 00 mov $0x0,%eax
17: c9 leave
18: c3 ret
00000019
19: 55 push %ebp
1a: 89 e5 mov %esp,%ebp
1c: 83 ec 20 sub $0x20,%esp
1f: c7 45 ec 01 00 00 00 movl $0x1,-0x14(%ebp)
26: c7 45 f0 02 00 00 00 movl $0x2,-0x10(%ebp)
2d: c7 45 ec 00 00 00 00 movl $0x0,-0x14(%ebp)
34: eb 0b jmp 41
36: c7 45 f4 04 00 00 00 movl $0x4,-0xc(%ebp)
3d: 83 45 ec 01 addl $0x1,-0x14(%ebp)
41: 83 7d ec 08 cmpl $0x8,-0x14(%ebp)
45: 7e ef jle 36
47: c7 45 f8 05 00 00 00 movl $0x5,-0x8(%ebp)
4e: c7 45 fc 06 00 00 00 movl $0x6,-0x4(%ebp)
55: b8 00 00 00 00 mov $0x0,%eax
5a: c9 leave
5b: c3 ret
我得出的结论是:在没有优化的情况下(优化之后可能会将变量x放在寄存器中),无论是将x放在for循环内部还是外部,其实都是一样的,x都是放在栈空间中的,而且对其的引用都是直接引用其地址,而不是说,每声明定义一次都需要动态的“为其分配地址空间”,本质上,函数内部的所有局部变量,都在编译时期确定了栈空间大小和其在栈空间中的地址。所以效率上来说,for循环内的局部变量,无论是放在循环外部声明定义还是内部,应该都是一样的,即使存在硬件高速缓冲,我想,对此也没有影响。所以,是要将变量的声明定义放在循环内部还是外部,视代码的可读性而言的。我想,可能优化的时候,应该将循环内定义声明的变量,在执行完循环之后,重用其栈空间。但如上的反汇编代码所示(没有优化),编译器并没有重用x的栈空间给y或者yy。
如下实验:
C代码:
#include
int sum(int a,char b,char c)
{
return 0;
}
int sum1(int a,char b,char c,char g)
{
return 0;
}
int main(void)
{
int a,b;
a = 1;
b = 2;
sum(1,2,3);
sum1(4,5,6,7);
int y = 4,yy=6;
return 0;
}
反汇编:
b.o: 文件格式 elf32-i386
Disassembly of section .text:
00000000
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 08 sub $0x8,%esp
6: 8b 55 0c mov 0xc(%ebp),%edx
9: 8b 45 10 mov 0x10(%ebp),%eax
c: 88 55 fc mov %dl,-0x4(%ebp)
f: 88 45 f8 mov %al,-0x8(%ebp)
12: b8 00 00 00 00 mov $0x0,%eax
17: c9 leave
18: c3 ret
00000019
19: 55 push %ebp
1a: 89 e5 mov %esp,%ebp
1c: 83 ec 0c sub $0xc,%esp
1f: 8b 4d 0c mov 0xc(%ebp),%ecx
22: 8b 55 10 mov 0x10(%ebp),%edx
25: 8b 45 14 mov 0x14(%ebp),%eax
28: 88 4d fc mov %cl,-0x4(%ebp)
2b: 88 55 f8 mov %dl,-0x8(%ebp)
2e: 88 45 f4 mov %al,-0xc(%ebp)
31: b8 00 00 00 00 mov $0x0,%eax
36: c9 leave
37: c3 ret
00000038
38: 55 push %ebp
39: 89 e5 mov %esp,%ebp
3b: 83 ec 20 sub $0x20,%esp
3e: c7 45 f0 01 00 00 00 movl $0x1,-0x10(%ebp)
45: c7 45 f4 02 00 00 00 movl $0x2,-0xc(%ebp)
4c: c7 44 24 08 03 00 00 movl $0x3,0x8(%esp)
53: 00
54: c7 44 24 04 02 00 00 movl $0x2,0x4(%esp)
5b: 00
5c: c7 04 24 01 00 00 00 movl $0x1,(%esp)
63: e8 fc ff ff ff call 64
68: c7 44 24 0c 07 00 00 movl $0x7,0xc(%esp)
6f: 00
70: c7 44 24 08 06 00 00 movl $0x6,0x8(%esp)
77: 00
78: c7 44 24 04 05 00 00 movl $0x5,0x4(%esp)
7f: 00
80: c7 04 24 04 00 00 00 movl $0x4,(%esp)
87: e8 fc ff ff ff call 88
8c: c7 45 f8 04 00 00 00 movl $0x4,-0x8(%ebp)
93: c7 45 fc 06 00 00 00 movl $0x6,-0x4(%ebp)
9a: b8 00 00 00 00 mov $0x0,%eax
9f: c9 leave
a0: c3 ret
可见,对于被调用函数的参数栈空间,是在父函数中分配的,按理说应该由被调用函数或者父函数在调用结束后清理,但是从汇编代码来看,并没有对其进行清理,而是不断由被调用函数重用。所以,函数内的栈空间分为两部分,一部分为本地局部变量分配空间,以16字节为单位;另一部分为被调用函数的参数栈空间分配,且以4字节为单位分配。而且,第二部分栈空间的大小以被调用的所有函数中,参数数量最多的,即占用参数栈空间最大的函数决定。所以,以上的main函数按照sum1函数的参数栈大小为其分配了16字节的栈空间,而sum函数参数栈空间占用12字节,但编译器并不为其单独分配12字节的空间,而是共用那16字节的参数栈空间。
如下实验:
C代码:
#include
int sum(int a,char b,unsigned char c)
{
printf("c=%d\n",c);
return 0;
}
int main(void)
{
int a,b;
a = 1;
b = 2;
long long c = -3;
sum(1,2,c);
return 0;
}
反汇编:
b.o: 文件格式 elf32-i386
Disassembly of section .text:
00000000
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 18 sub $0x18,%esp
6: 8b 55 0c mov 0xc(%ebp),%edx
9: 8b 45 10 mov 0x10(%ebp),%eax
c: 88 55 f4 mov %dl,-0xc(%ebp)
f: 88 45 f0 mov %al,-0x10(%ebp)
12: 0f b6 45 f0 movzbl -0x10(%ebp),%eax
16: 89 44 24 04 mov %eax,0x4(%esp)
1a: c7 04 24 00 00 00 00 movl $0x0,(%esp)
21: e8 fc ff ff ff call 22
26: b8 00 00 00 00 mov $0x0,%eax
2b: c9 leave
2c: c3 ret
0000002d
2d: 55 push %ebp
2e: 89 e5 mov %esp,%ebp
30: 83 e4 f0 and $0xfffffff0,%esp
33: 83 ec 20 sub $0x20,%esp
36: c7 44 24 10 01 00 00 movl $0x1,0x10(%esp)
3d: 00 11111111
3e: c7 44 24 14 02 00 00 movl $0x2,0x14(%esp)
45: 00
46: c7 44 24 18 fd ff ff movl $0xfffffffd,0x18(%esp)
4d: ff
4e: c7 44 24 1c ff ff ff movl $0xffffffff,0x1c(%esp)
55: ff
56: 8b 44 24 18 mov 0x18(%esp),%eax
5a: 0f b6 c0 movzbl %al,%eax
5d: 89 44 24 08 mov %eax,0x8(%esp)
61: c7 44 24 04 02 00 00 movl $0x2,0x4(%esp)
68: 00
69: c7 04 24 01 00 00 00 movl $0x1,(%esp)
70: e8 fc ff ff ff call 71
75: b8 00 00 00 00 mov $0x0,%eax
7a: c9 leave
7b: c3 ret
printf打印的c值为253。为啥?因为调用函数的时候,传递的实参c为long long类型(signed),占8个字节46到4e行是-3的补码形式,即计算机内部是以补码形式存放数值的。编译器使用movzbl指令将c参数入栈,因为sum函数声明中c的类型为unsigned char类型,所以先将long long类型强制类型转换,即取其低位一个字节并填充高三个字节为0入栈4个字节(使用movzbl,将其高三个字节填充为0,而低一个字节取long long的低一个字节)。因为c为-3,所以入栈的四个字节为0x000000fd(补码形式),sum中取其fd即11111101,也即253。这里我想表达的是,计算机内部使用的是补码,额。。。。。。
最后一个实验:
C代码:
#include
char sum(int a,char b,char c)
{
char s = b+c;
return s;
}
int main(void)
{
int a,b;
a = 1;
b = 2;
sum(1,2,a);
return 0;
}
反汇编:
b.o: 文件格式 elf32-i386
Disassembly of section .text:
00000000
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 18 sub $0x18,%esp
6: 8b 55 0c mov 0xc(%ebp),%edx
9: 8b 45 10 mov 0x10(%ebp),%eax
c: 88 55 ec mov %dl,-0x14(%ebp) #这里是将父函数中的char类型的实参又复制了一份到本地函数栈中
f: 88 45 e8 mov %al,-0x18(%ebp) #同上
12: 0f b6 55 ec movzbl -0x14(%ebp),%edx
16: 0f b6 45 e8 movzbl -0x18(%ebp),%eax
1a: 01 d0 add %edx,%eax #将两个char类型的变量当作两个int类型的变量来参与计算,即“类型提升”
1c: 88 45 ff mov %al,-0x1(%ebp)
1f: 0f b6 45 ff movzbl -0x1(%ebp),%eax
23: c9 leave
24: c3 ret
00000025
25: 55 push %ebp
26: 89 e5 mov %esp,%ebp
28: 83 ec 1c sub $0x1c,%esp
2b: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%ebp)
32: c7 45 fc 02 00 00 00 movl $0x2,-0x4(%ebp)
39: 8b 45 f8 mov -0x8(%ebp),%eax
3c: 0f be c0 movsbl %al,%eax #相当于强制类型转换,将int类型转换为char类型,并置于一个4字节的内存单元中入栈(a)
3f: 89 44 24 08 mov %eax,0x8(%esp)
43: c7 44 24 04 02 00 00 movl $0x2,0x4(%esp) #直接将2作为4字节的int入栈(类型提升)
4a: 00
4b: c7 04 24 01 00 00 00 movl $0x1,(%esp)
52: e8 fc ff ff ff call 53
57: b8 00 00 00 00 mov $0x0,%eax
5c: c9 leave
5d: c3 ret
关于类型提升:
1.在32bit系统上,函数参数不管是char,short,int都是以4byte压入栈中。被调函数再根据定义,把实参裁减为定义的类型。从这句话来看,被调函数内部确实有必要将父函数中的实参“裁减”并复制到本地参数堆栈中。因为在C语言中,父函数所在的文件中可能没有被调函数的声明,所以这种情况下在父函数中并不会对入栈的参数进行“裁减”。
2.整型提升就是char,short(无论unsigned,signed),位段类型,枚举类型都将提升为int类型。前提是int类型能完整容纳原先的数据,否则提升为unsigned int类型。
3.类型提升时,高位填充的规则,若提升的目的类型为unsigned,则填充0,使用movzbl汇编指令;若提升的目的类型为signed类型,则填充符号位。
一个简单的例子:(来自http://blog.chinaunix.net/uid-23629988-id-292647.html)
/***************************************************************/
int main()
{
int i;
unsigned char *p;
char *p1;
int a[] = {0xffffffff, 0xffffffff, 0xffffffff};
p = a;
p1 = a;
for(i = 0 ; i < 8 ; i++) {
printf(" 0x%02x 0x%02x \n", p[i], p1[i]);
}
}
$ gcc main.c
main.c: In function ‘main’:
main.c:10: warning: assignment from incompatible pointer type
main.c:11: warning: assignment from incompatible pointer type
$ ./a.out
0xff 0xffffffff
0xff 0xffffffff
0xff 0xffffffff
0xff 0xffffffff
0xff 0xffffffff
0xff 0xffffffff
0xff 0xffffffff
0xff 0xffffffff
。。。。。。 。。。。。。
/***************************************************************/
根本原因其实很简单。
%x是打印无符号整数的16进制,而例子中传递的类型是字符型,那么这里就有一个字符提升的问题,将类型提升为无符号整形。
*p是unsigned char,其值为0xff,那么对应的无符号整形的值仍然是0xff。
而*p1确实char,其值为0xff,其对应的无符号整形的值为0xffffffff。为什么这次是0xffffffff呢?
因为*p1为-1,而无符号整数的-1则是0xffffffff。
为什么是这样呢?
因为在在编码为补码的情形下,类型提升有两种情况:
1. 符号扩展:对于有符号数,扩展存储位数的方法。在新的高位字节使用当前最高有效位即符号位的值进行填充。
2. 零扩展:对于无符号数,扩展存储位数的方法。在新的高位直接填0.
对于这个例子来说。*p是无符号数,所以填充的是0,即为0x000000ff。而*p1是有符号数,所以填充的是1,即为0xffffffff。
因此,从char型到unsigned int,是对有符号数的提升,因此用的是符号扩展,oxff被扩展为oxffffffff;而从unsigned char型到unsigned int型,是对无符号数的扩展,使用零扩展,oxff被扩展为ox000000ff,而填充的这些零是不会被打印出来的。
如果说这样教科书式的概念不容易理解。还有这样一种理解方式,也许不一定准确,但更容易理解。
对于这里的类型提升,整个步骤可以这样理解:
1. %x要求参数为无符号整数,需要参数为4个字节;
2. *p, *p1为(unsigned) char型,只占1个字节;
3. 因为参数的类型不符,需要扩展;
4. 定位需要扩展到4个字节;
5. 那么就需要填充增加的3个字节;
6. 这3个字节需要什么值?这里就需要上面所需要的概念了。针对有符号数和无符号数,进行不同值的填充。