写在开头以免看到结尾你
,此篇博客纯属瞎扯,看看就可以了,不要当真哦!
如果搞过汇编,写过子程序,那么你就不用看了,因为看到最后你会发现,在汇编中你有很多方法去返回值,传递参数,而在高级语言中,编译器只是选择了其中的一种而已,而这篇博客也写的毫无逻辑,简直丧尽天良,草菅人命,道却也有那么点点意思,如果你能看完我叫你大哥
,,,
老规矩,先热下身,简简单单的几行代码(额,第一次linux下看AT&T汇编的哦,以前win下intel汇编,但是不管什么平台,基本靠猜
,,,)
Gcc一下生成a.out ,objdump –Da.out 生成反汇编代码,就成芥样子啦,看不懂吧,我也是,但第一行还是能猜出来的,x86构架下64位elf可执行文件,我不会,但是我会猜呀,,,
man 一下objdump看一下-D 参数的介绍是查看elf可执行文件的所有段
搜索一下看一下有没有.data段.bbs段,正如man的介绍-D参数是包含所有段的,在此我们只关注.text段的main函数与fun函数()
搜索一下main,会发现fun函数就在main函数的上边,可能跟我声明定义fun函数的位置有关
通过反汇编代码我们可以看到每一个函数进入后都有
push %rbp
mov %rsp,%rbp
但是main函数的代码中多了sub $0x10,%rsp ,这行代码是干啥的?学win32汇编时你应该知道,ebp,esp是干啥的,这里只是换了个名r代表64位,e代表32位。其实这是给局部变量分配栈空间,fun也是函数也有局部变量,为傻没有这行代码?(对比main与fun的代码,我猜测可能只有函数内调用了函数才会有这行代码)。从C代码中可以看到我们只在main函数中定义了一个占用一个字节的char类型变量,但编译器却分配了16个字节的栈空间(猜测可能是gcc编译器默认不超过16字节,会分配16个字节作为局部变量的栈空间,其实可能就是这样)
用实验证明两个猜测:
第一个猜测,猜测只有函数内调用了函数才会有sub $0x10,%rsp这行代码,既然fun函数内没有函数调用,那我们就加一个函数调用,修改代码如下:
反汇编后如下
喔喔喔,还真是这样额,一猜一个准
其实就是为了给每一个函数调用生成一个栈帧而已,,,,
接下来验证第二个猜想:可能是gcc编译器默认不超过16字节,会分配16个字节作为局部变量的栈空间
修改代码如下:
反汇编一下
通过反汇编代码可以看出,在此平台下一个int类型变量4个字节,一个short类型变量是3个字节(我记得书本上说short好像是2个字节奥,但此处编译器分配了3个字节),而一个char类型正如大多数C语言课本上说的是1个字节。
先不管这个,不管怎么说我们已经定义了超过16字节栈空间的局部变量了,而sub $0x10,%rsp 也变成了sub $0x20,%rsp ,由此我们可以推断出我们现在使用的gcc编译器,为局部变量分配栈空间的一个默认规律,以16字节的整数倍去为局部变量分配栈空间,不足16字节,也分配16字节,,,
接下来我们再来说字节问题,由反汇编代码可知,虽然编译器为short类型变量分配了3个字节栈空间,但为其生成的赋值汇编代码却是movw $0x5,-0x12(%rbp) ,mov后加了一个w后缀,通过各个变量首地址的对比与变量类型可以推断出:w代表2字节,b代表1字节,l代表4字节,
也就是说,虽然编译器为short类型变量分配了3个字节栈空间,但short类型变量还是遵循C语言short类型2个字节的标准的,生成的汇编代码只会去存取首地址开始的2个字节。虽然这样但你可不要期望用一个占用3个字节的16位的short类型变量可以容纳一个最大的24位的变量,你还可以什么都不用做就可以正确的访问到这个最大的24位的变量事实真是这样?不好奕屎这只是我的猜测,还有待测试去证明,,,,
再次观察反汇编代码
4004a6: c745 f4 03 00 00 00 movl $0x3,-0xc(%rbp) int d = 3
4004ad: c645 fb 04 movb $0x4,-0x5(%rbp) char e = 4
4004b1: 66c7 45 fc 05 00 movw $0x5,-0x4(%rbp) short f = 5
4004b7: b800 00 00 00 mov $0x0,%eax
4004bc: e8b3 ff ff ff callq 400474<fun>
4004c1: 8845 ff mov %al,-0x1(%rbp) char b = fun()
你会发现char类型变量e与int类型d变量之间4有3个字节的空隙,char类型变量b与short类型变量f之间有1个字节的空隙,这些个字节是干啥的?我想起了C语言课本上的字节对齐,哈哈哈哈啊哈啊哈啊哈,,,,,
修改代码测试下,我们用事实说话,不要瞎猜
反汇编对比一下,整整少了16字节的栈空间,仅仅只是调整了一下char类型与short类型定义的顺序而已,想象一下如果我们在此用第一次定义变量的顺序去声明一个结构体或一个c++类,我们在堆中大量创建这个类型的变量,会浪费多少内存?
测试一下,修改代码如下
运行一下,多了整整4个字节哦,你要知道玩过51单片机的程序员有多抠门,毕竟才128b的ram,4kb的rom,,,
关于字节对齐,简单点就是基本类型所分配的栈空间的首地址要能被自身大小(字节)所整除,关于结构体对齐可百度,,,
不说了,说了这么多还没到正题,你的身热了?
废话真多,说好的是看一下,函数是怎么返回值和传递参数的?
整理下发型,现正我们峰回路转,言归正传,,,
真是柳暗花明另一村!村!村!村!啊!
我们先看一下当函数返回一个字节的时候gcc编译器是怎样生成汇编代码的?
让我们在重温一下这篇垃圾博客开始的那几行看似简单,其实确实很简单的几行代码,,,
从1和4可以看到char 类型变量b与a都在main和fun函数调用时,编译器为其分配的栈空间的第一个字节-0x1(%rbp)处
1:movb $0x1,-0x1(%rbp) 变量赋值语句 char a= 1
2:movzbl-0x1(%rbp),%eax 变量返回语句 returna
3:callq 400474 <fun> 函数fun调用语句
4:mov %al,-0x1(%rbp) char b = fun() 接收fun函数返回值
Rax,eax,ax,al,ah:64,32,16,8,8
从1-4我们不难看出,编译器在返回一个字节的变量时,会把此变量放到eax寄存器内,然后在返回到主调函数后会把这一个字节的变量从ax寄存器的低8位也就是1个字节复制到主调函数的栈空间-0x1(%rbp)也就是接收变量b中
既然一个字节是这样返回,那么2,4,8 个字节肯定也是这样返回的,不用测应该肯定就是这样,因为64位cpu,rax 寄存器有8字节
那么问题来了,返回了一个超过了一个寄存器能容纳的返回值(返回c++对象(map,list,arry),结构体,数组),编译器是怎样返回的?
不用测,我就知道,返回被调函数中存放此变量栈的首地址(64位下最大的地址也不过8字节而已)就行了,然后在主调函数中再把此首地址开始的sizeof(此变量)字节,拷贝到主调函数中接收此变量的栈空间上
事实真是这样?还是测一下吧
修改代码如下
反汇编一下,喔!fun函数太长啦,直接上代码
0000000000400581 <main>:
58 400581: 55 push %rbp
59 400582: 48 89 e5 mov %rsp,%rbp
60 400585: 48 83 ec 60 sub $0x60,%rsp
61 400589: 488d 45 a0 lea -0x60(%rbp),%rax
62 40058d: 4889 c7 mov %rax,%rdi
63 400590: b8 00 00 00 00 mov $0x0,%eax
64 400595: e8 da fe ff ff callq 400474 <fun>
65 40059a: c9 leaveq
66 40059b: c3 retq
67 40059c: 90 nop
先来看一下mian函数,发现比调用返回一个字节的fun函数多了61,62两行,这两条指令的作用是把-0x60(%rbp)的地址放到rax寄存器(编译器为接收返回值结构体变量b分配的栈空间首地址),然后再把此地址放到rdi寄存器内
然后我们来看一下fun函数,发现比调用返回一个字节的fun函数多了太多行
0000000000400474 <fun>:
1 400474: 55 push %rbp
2 400475: 48 89 e5 mov %rsp,%rbp
3 400478: 53 push %rbx
4 400479: 4889 fa mov %rdi,%rdx
5 40047c: 488d 5d 90 lea -0x70(%rbp),%rbx
6 400480: b800 00 00 00 mov $0x0,%eax
7 400485: b90c 00 00 00 mov $0xc,%ecx
8 40048a: 4889 df mov %rbx,%rdi
9 40048d: f348 ab rep stos %rax,%es:(%rdi)
10 400490: c7 45 90 01 00 00 00 movl $0x1,-0x70(%rbp)
11 400497: c7 45 94 02 00 00 00 movl $0x2,-0x6c(%rbp)
12 40049e: c7 45 98 03 00 00 00 movl $0x3,-0x68(%rbp)
13 4004a5: c7 45 9c 04 00 00 00 movl $0x4,-0x64(%rbp)
14 4004ac: c7 45 a0 05 00 00 00 movl $0x5,-0x60(%rbp)
15 4004b3: c7 45 a4 06 00 00 00 movl $0x6,-0x5c(%rbp)
16 4004ba: c7 45 a8 07 00 00 00 movl $0x7,-0x58(%rbp
17 4004c1: c7 45 ac 08 00 00 00 movl $0x8,-0x54(%rbp)
18 4004c8: c7 45 b0 09 00 00 00 movl $0x9,-0x50(%rbp)
19 4004cf: c745 b4 0a 00 00 00 movl $0xa,-0x4c(%rbp)
20 4004d6: c7 45 b8 0b 00 00 00 movl $0xb,-0x48(%rbp)
21 4004dd: c7 45 bc 0c 00 00 00 movl $0xc,-0x44(%rbp)
22 4004e4: c7 45 c0 0d 00 00 00 movl $0xd,-0x40(%rbp)
23 4004eb: c7 45 c4 0e 00 00 00 movl $0xe,-0x3c(%rbp)
24 4004f2: c7 45 c8 0f 00 00 00 movl $0xf,-0x38(%rbp)
25 4004f9: c7 45 cc 10 00 00 00 movl $0x10,-0x34(%rbp)
26 400500: c7 45 d0 11 00 00 00 movl $0x11,-0x30(%rbp)
27 400507: c7 45 d4 12 00 00 00 movl $0x12,-0x2c(%rbp)
28 40050e: c7 45 d8 13 00 00 00 movl $0x13,-0x28(%rbp)
29 400515: c7 45 dc 14 00 00 00 movl $0x14,-0x24(%rbp)
30 40051c: 48 8b 45 90 mov -0x70(%rbp),%rax
31 400520: 48 89 02 mov %rax,(%rdx)
32 400523: 48 8b 45 98 mov -0x68(%rbp),%rax
33 400527: 48 89 42 08 mov %rax,0x8(%rdx)
34 40052b: 48 8b 45 a0 mov -0x60(%rbp),%rax
35 40052f: 48 89 42 10 mov %rax,0x10(%rdx)
36 400533: 48 8b 45 a8 mov -0x58(%rbp),%rax
37 400537: 48 89 42 18 mov %rax,0x18(%rdx)
38 40053b: 48 8b 45 b0 mov -0x50(%rbp),%rax
39 40053f: 48 89 42 20 mov %rax,0x20(%rdx)
40 400543: 48 8b 45 b8 mov -0x48(%rbp),%rax
41 400547: 48 89 42 28 mov %rax,0x28(%rdx)
42 40054b: 48 8b 45 c0 mov -0x40(%rbp),%rax
43 40054f: 48 89 42 30 mov %rax,0x30(%rdx)
44 400553: 48 8b 45 c8 mov -0x38(%rbp),%rax
45 400557: 48 89 42 38 mov %rax,0x38(%rdx)
46 40055b: 48 8b 45 d0 mov -0x30(%rbp),%rax
47 40055f: 48 89 42 40 mov %rax,0x40(%rdx)
48 400563: 48 8b 45 d8 mov -0x28(%rbp),%rax
49 400567: 48 89 42 48 mov %rax,0x48(%rdx)
50 40056b: 48 8b 45 e0 mov -0x20(%rbp),%rax
51 40056f: 48 89 42 50 mov %rax,0x50(%rdx)
52 400573: 48 8b 45 e8 mov -0x18(%rbp),%rax
53 400577: 48 89 42 58 mov %rax,0x58(%rdx)
54 40057b: 48 89 d0 mov %rdx,%rax
55 40057e: 5b pop %rbx
56 40057f: c9 leaveq
57 400580: c3 retq
第3行push %rbx说明接下来的代码的第5行lea -0x70(%rbp),%rbx会用到rbx寄存器,所以现在要把rbx的值存到fun函数的栈中,我们在函数末可以看到第55行pop %rbx又把原值从栈中谈到rbx中啦
第4行mov %rdi,%rdx 把main函数中结构体变量b的首地址放到rdx寄存器中
第5-9行 是初始化栈空间的,大意就是把从此基栈指针-0x70到基栈指针-0x10这段栈空间,分$0xc(12)次,每次初始化8字节(清0)(32位vc编译器会用0cccccccch去初始化栈空间)
第10-29行 是为fun函数临时结构体变量a内的各个变量赋值
接下来重点来了,要开始返回结构体啦
第30行 mov -0x70(%rbp),%rax 把从-0x70(%rbp)地址开始的8个字节(a,d[0])放到rax寄存器中,别问我是蒸馍知道是8个字节的,我们可以看一下31,33,35,行指针的变化情况就可以推断出来啦,而且我们还可以断定当mov指令的源操作数是一个地址,目的操作数是一个寄存器时,mov指令会移动此地址开始的目的操作数寄存器大小字节的数据到此寄存器中,,,
第31行 mov %rax,(%rdx) 把rax寄存器中的值(a,d[0])放到rdx存储的地址上,也就是把fun函数中的临时结构体变量a的第一个元素a,和第二个元素数组d的第一个元素d[0],拷贝到main函数中结构体变量b中
接下来的32-53行就是每次8字节的把,fun函数栈空间内的临时结构体变量a拷贝到,main函数的栈空间内的临时结构体变量b中
第54行mov %rdx,%rax 把main函数中栈空间内的临时结构体变量b的地址放到rax寄存器内(这个有啥用?????)
验证下来,发现和我的猜想还是有区别的
我想的是被调函数返回临时变量的地址到主调函数,主调函数再拷贝
而事实是主调函数把接收变量的地址传到被调函数中,拷贝过程在被调函数中实现,这就好比我们显示的把接收变量的指针当做参数传递给被调函数
这就像c语言的cdel 与c++的 stdcall 一样,函数调用过程中的参数产生的临时栈空间是由调用者清除,还是被调用者清除,,,,
这样设计有一个好处就是在C++中可以直接在return时创建对象,这样会比先创建对象再返回效率高,直接return 时创建对象,会在主调函数的栈空间中分配内存,然后再在此块内存上调用构造函数构造对象。而先创建对象,再return ,会在被调函数的栈空间分配内存调用构造函数构造对象,然后在return时再调用对象的拷贝构造函数,把对象拷贝构造到主调函数的栈空间,还要调用被调函数临时对象的析构函数(我只是这样猜测,不同编译器实现可能不同,你可以去测一下),,,
其实事实并不是这样的,你这人怎么这样?
事实是只有当被返回的变量的大小大于所有可用的通用寄存器(不同的编译器,不同的cpu构架下可能会选取几个通用寄存器用来传递参数和返回值,就像此处x86构架下gcc编译器返回少于8字节是会用rax,eax等寄存器,vc下会用eax传递c++ this指针,g++下会用rdi寄存器传递this指针)的大小后才会使用这种方式,
修改代码如下验证一下gcc编译器是否会用寄存器,传递参数and返回值:
structtest{
1 int a[2];
2 char b[2];
3 short c;
};
struct test fun(struct test a)
{
4 return a;
}
void main()
{
5 struct test a={0,1,2,3,4};
6 struct test b= fun(a);
7 b.a[0]=1;
8 b.c=2;
}
0000000000400474 <fun>:
9 400474: 55 push %rbp
10 400475: 48 89 e5 mov %rsp,%rbp
11 400478: 48 89 fa mov %rdi,%rdx
12 40047b: 89 f0 mov %esi,%eax
13 40047d: 48 89 55 e0 mov %rdx,-0x20(%rbp)
14 400481: 89 45 e8 mov %eax,-0x18(%rbp)
15 400484: 48 8b 45 e0 mov -0x20(%rbp),%rax
16 400488: 48 89 45 f0 mov %rax,-0x10(%rbp)
17 40048c: 8b 45 e8 mov -0x18(%rbp),%eax
18 40048f: 89 45 f8 mov %eax,-0x8(%rbp)
19 400492: 48 8b 45 f0 mov -0x10(%rbp),%rax
20 400496: 8b 55 f8 mov -0x8(%rbp),%edx
21 400499: c9 leaveq
22 40049a: c3 retq
000000000040049b <main>:
23 40049b: 55 push %rbp
24 40049c: 48 89 e5 mov %rsp,%rbp
25 40049f: 48 83 ec 30 sub $0x30,%rsp
26 4004a3: c745 f0 00 00 00 00 movl $0x0,-0x10(%rbp)
27 4004aa: c745 f4 01 00 00 00 movl $0x1,-0xc(%rbp)
28 4004b1: c645 f8 02 movb $0x2,-0x8(%rbp)
29 4004b5: c645 f9 03 movb $0x3,-0x7(%rbp)
30 4004b9: 66c7 45 fa 04 00 movw $0x4,-0x6(%rbp)
31 4004bf: 488b 55 f0 mov -0x10(%rbp),%rdx
32 4004c3: 8b45 f8 mov -0x8(%rbp),%eax
33 4004c6: 4889 d7 mov %rdx,%rdi
34 4004c9: 89c6 mov %eax,%esi
35 4004cb: e8 a4 ff ff ff callq 400474 <fun>
36 4004d0: 48 89 c1 mov %rax,%rcx
37 4004d3: 89 d0 mov %edx,%eax
38 4004d5: 48 89 4d d0 mov %rcx,-0x30(%rbp)
39 4004d9: 89 45 d8 mov %eax,-0x28(%rbp)
40 4004dc: 48 8b 45 d0 mov -0x30(%rbp),%rax
41 4004e0: 48 89 45 e0 mov %rax,-0x20(%rbp)
42 4004e4: 8b 45 d8 mov -0x28(%rbp),%eax
43 4004e7: 89 45 e8 mov %eax,-0x18(%rbp)
44 4004ea: c7 45 e0 01 00 00 00 movl $0x1,-0x20(%rbp)
45 4004f1: 66 c7 45 ea 02 00 movw $0x2,-0x16(%rbp)
46 4004f7: c9 leaveq
在mian函数中我们定义了一个12字节大小的结构体变量a,并初始化了它,然后把变量a当做参数传给fun函数,并在fun函数中返回此变量到mian函数的结构体变量b中
第26-30行以给定的值初始化结构体变量a
第31行 mov -0x10(%rbp),%rdx 把以-0x10(%rbp)为首地址的8个字节(int类型数组a)放到64位rdx寄存器中
第32行 mov -0x8(%rbp),%eax 把以-0x8(%rbp)为首地址的4个字节(char类型数组b,和short 类型变量c)放到32位eax寄存器中
第33行 mov %rdx,%rdi 把int类型数组a放到rdi寄存器
第34行 mov %eax,%esi char类型数组b,和short 类型变量c 放到esi寄存器中
31-34其实是把12字节的结构体变量a分8字节和4字节分别放到rdi,esi寄存器中当做fun函数的参数(vc6.0使用的编译器一般会把参数放到栈中而不是用寄存器去传递参数,32位系统下传递c++this指针是会用eax寄存器去传递的)
第11-20行生成了这么多代码,就是为了把rdi中的int类型数组a放到rax寄存器,把esi寄存器中的char类型数组b,和short 类型变量c 放到edx寄存器中,作为返回值
第36-43 同11-20是为了把fun函数返回的rax,edx,寄存器中的中值放到mian函数接收结构体变量b中
百思不得其解,明明11-20行和36-43行,2行代码就可以搞定的事,为何要搞这么多代码,传过来,传过去的,,,
不管怎么说我们已经看到函数返回值时,gcc编译器会用寄存器返回值或值太大把接收变量的首地址放到rdi寄存器传递到被调函数,然后在被调函数中直接拷贝返回值到接收变量中,传递参数时也会使用寄存器,至于什么时候会用栈去传递参数,留给你去测吧,,,
搞到这其实我又有一个猜想,我们在c,c++中返回值时,大必不用声明返回什么类型,我们只需要随便返回一个指针就行了。
1. 因为函数返回前我们并未在反汇编代码中看到栈清0操作,这也同时提醒我们在做一些敏感信息的处理时要记得及时覆盖敏感数据。
2. 因为进程是操作系统分配资源的最小单位,线程是资源调度的最小单位,但线程同时还拥有自己的栈空间和被调度运行时占用的cpu寄存器,栈是线程私有的,你在线程内调用一个函数后,在下一次调用函数之前,把上一个函数调用的栈中的东西拷贝出来就行了,不用怕什么所谓的临时变量,函数返回后就不存在了,它一直都在,等着你再次在此线程内调用函数产生栈帧去覆盖它(在C++中函数返回后栈空间的临时对象的析构函数会被执行哦),,,,,
这样我们就可以显示的编写代码完成函数返回值的拷贝,而不是要编译器去生成拷贝代码
测一下修改代码如下
在主调函数main中拷贝fun,fun0,fun1函数返回后,其栈帧中的临时结构体变量a到主调函数main栈帧中的int类型数组x中
可以看到编译器在编译时会警告,函数返回了一个临时变量的地址,返回指针类型与接收类型不匹配,懒得理你额,我们看运行结果,函数返回后我们还是可以去访问这个地址取得变量的值的哦,,,,
我是真的真的是没骗你的哦!我是真的真的只是在猜测后才验证的哦,,,
方框1我们不论返回什么指针我们都可以在函数返回后继续访问此函数fun,fun0中的临时结构体变量a
方框2我们使用memcpy想把函数fun1中的临时结构体变量a拷贝到main函数中的数组x中,但运行结果表明我们失败了,因为我们在拷贝过程中又调用了函数,memcpy函数调用过程中产生的栈帧覆盖了fun1函数的栈帧
方框3,4,5 在不调用拷贝函数的情况下使用了3种方法,还可以有更多种(其实都是利用变量间的赋值)去拷贝fun1函数中的临时结构体变量a到main函数临时数组x中
可以看到在3,4,5方框我们去调用fun1后根本没接收其返回值,但是我们还是可以通过方框2中的变量b去访问每一次fun1函数调用后的栈帧中的临时结构体变量a,在方框2中我们第一次调用fun1其返回的临时结构体变量a的地址与其后每一次调用fun1中的临时结构体变量a的地址一直没改变过,同一个函数在同一个地方无论你重复调用多少次其栈帧中各个临时变量的相对地址都是不会变的,前提是中间没穿插其它函数调用,,,
结论就是:其它我不论你什么c++,java,python,你什么编译型,解释性,什么指针,引用,对象,套路就这么几样,最终都会变成cpu指令,只不过虚拟机会把自己的字节码解释成当前cpu指令,都差不多哦,如果你测了,结果不是,你可以来大石窝
,,,
由于编译器把高级语言中我们所谓的变量,和地址关联起来了,你可以看到,反汇编中没什么变量,全是地址,,,
指针是个好玩的东西,,,,,,