接之前的博客,废话不多说,直接上代码分析
C代码
#include <stdio.h>
using namespace std;
void test() {
printf("\n!!!执行了 test 函数!!!\n\n");
return;
}
int sumset(int x, int y, int i) {
int a = x;
int b = y;
void* c[1];
printf("\nsumset(x, y, i) 参数 x 的地址是: %lx \n", (unsigned long long)&x);
printf("\nsumset(x, y, i) 参数 y 的地址是: %lx \n", (unsigned long long)&y);
printf("\nsumset(x, y, i) 参数 i 的地址是: %lx \n", (unsigned long long)&i);
printf("\nsumset 函数局部变量 a 的地址是: %lx \n", (unsigned long long)&a);
printf("\nsumset 函数局部变量 b 的地址是: %lx \n", (unsigned long long)&b);
printf("\nsumset 函数局部变量 c 的地址是: %lx \n\n", (unsigned long long)&c);
for(int k=0; k<=i+1; k++) {
printf("c[%d] = %16lx \n", k, (unsigned long long)c[k]);
}
printf("\nc[%d] 中的原值 %lx 将被赋予新值 %lx \n", i, (unsigned long long)c[i], (unsigned long long)&test);
c[i] = (void*)&test;
return a+b;
}
int main() {
int a;
printf("\nmain 函数的地址是: %lx \n", (unsigned long long)&main);
printf("\nsumset 函数的地址是: %lx \n", (unsigned long long)&sumset);
printf("\ntest 函数的地址是: %lx \n", (unsigned long long)&test);
while(1) {
printf("\n输入一个整数(负数将退出程序):");
scanf("%d", &a);
if(a < 0) break;
printf("\n调用函数 sumset(%d, %d, %d) \n", a, a, a);
sumset(a, a, a);
}
return 0;
}
乍一看这段代码可能不知道它是干嘛的,我们可以编译运行它查看结果
我们可以看到各个函数、参数及局部变量的地址,接着输入
直到输入4完了以后,输入5,程序忽然崩溃而直接结算进程了
是输入4的问题还是输入5的问题?我们单独试试输入5
程序成功运行了,不过执行了test函数,然后结束了,这是为什么呢???
这里先告诉大家输入4之后输入任何数程序都会结束,有兴趣的同学可以自己试一试~(其实是懒得截图)
我们通过调试器产生的汇编代码来分析分析这个过程:)
main函数
0x000000000040165e <+0>: push rbp
0x000000000040165f <+1>: mov rbp,rsp
0x0000000000401662 <+4>: sub rsp,0x30
0x0000000000401666 <+8>: call 0x4022a0 <__main>
0x000000000040166b <+13>: lea rax,[rip+0xffffffffffffffec] # 0x40165e <main()>
0x0000000000401672 <+20>: mov rdx,rax
0x0000000000401675 <+23>: lea rcx,[rip+0x2ad3] # 0x40414f
0x000000000040167c <+30>: call 0x402cc0 <printf>
0x0000000000401681 <+35>: lea rax,[rip+0xfffffffffffffec3] # 0x40154b <sumset(int, int, int)>
0x0000000000401688 <+42>: mov rdx,rax
0x000000000040168b <+45>: lea rcx,[rip+0x2ad9] # 0x40416b
0x0000000000401692 <+52>: call 0x402cc0 <printf>
0x0000000000401697 <+57>: lea rax,[rip+0xfffffffffffffe92] # 0x401530 <test()>
0x000000000040169e <+64>: mov rdx,rax
0x00000000004016a1 <+67>: lea rcx,[rip+0x2adf] # 0x404187
0x00000000004016a8 <+74>: call 0x402cc0 <printf>
0x00000000004016ad <+79>: lea rcx,[rip+0x2af4] # 0x4041a8
0x00000000004016b4 <+86>: call 0x402cc0 <printf>
0x00000000004016b9 <+91>: lea rax,[rbp-0x4]
0x00000000004016bd <+95>: mov rdx,rax
0x00000000004016c0 <+98>: lea rcx,[rip+0x2b00] # 0x4041c7
0x00000000004016c7 <+105>: call 0x402cc8 <scanf>
0x00000000004016cc <+110>: mov eax,DWORD PTR [rbp-0x4]
0x00000000004016cf <+113>: test eax,eax
0x00000000004016d1 <+115>: jns 0x4016d5 <main()+119>
0x00000000004016d3 <+117>: jmp 0x401707 <main()+169>
0x00000000004016d5 <+119>: mov ecx,DWORD PTR [rbp-0x4]
0x00000000004016d8 <+122>: mov edx,DWORD PTR [rbp-0x4]
0x00000000004016db <+125>: mov eax,DWORD PTR [rbp-0x4]
0x00000000004016de <+128>: mov r9d,ecx
0x00000000004016e1 <+131>: mov r8d,edx
0x00000000004016e4 <+134>: mov edx,eax
0x00000000004016e6 <+136>: lea rcx,[rip+0x2ae3] # 0x4041d0
0x00000000004016ed <+143>: call 0x402cc0 <printf>
0x00000000004016f2 <+148>: mov ecx,DWORD PTR [rbp-0x4]
0x00000000004016f5 <+151>: mov edx,DWORD PTR [rbp-0x4]
0x00000000004016f8 <+154>: mov eax,DWORD PTR [rbp-0x4]
0x00000000004016fb <+157>: mov r8d,ecx
0x00000000004016fe <+160>: mov ecx,eax
0x0000000000401700 <+162>: call 0x40154b <sumset(int, int, int)>
0x0000000000401705 <+167>: jmp 0x4016ad <main()+79>
0x0000000000401707 <+169>: mov eax,0x0
0x000000000040170c <+174>: add rsp,0x30
0x0000000000401710 <+178>: pop rbp
0x0000000000401711 <+179>: ret
sumset函数
0x000000000040154b <+0>: push rbp
0x000000000040154c <+1>: mov rbp,rsp
0x000000000040154f <+4>: sub rsp,0x40
0x0000000000401553 <+8>: mov DWORD PTR [rbp+0x10],ecx
0x0000000000401556 <+11>: mov DWORD PTR [rbp+0x18],edx
0x0000000000401559 <+14>: mov DWORD PTR [rbp+0x20],r8d
0x000000000040155d <+18>: mov eax,DWORD PTR [rbp+0x10]
0x0000000000401560 <+21>: mov DWORD PTR [rbp-0x8],eax
0x0000000000401563 <+24>: mov eax,DWORD PTR [rbp+0x18]
0x0000000000401566 <+27>: mov DWORD PTR [rbp-0xc],eax
0x0000000000401569 <+30>: lea rax,[rbp+0x10]
0x000000000040156d <+34>: mov rdx,rax
0x0000000000401570 <+37>: lea rcx,[rip+0x2aa9] # 0x404020
0x0000000000401577 <+44>: call 0x402cc0 <printf>
0x000000000040157c <+49>: lea rax,[rbp+0x18]
0x0000000000401580 <+53>: mov rdx,rax
0x0000000000401583 <+56>: lea rcx,[rip+0x2abe] # 0x404048
0x000000000040158a <+63>: call 0x402cc0 <printf>
0x000000000040158f <+68>: lea rax,[rbp+0x20]
0x0000000000401593 <+72>: mov rdx,rax
0x0000000000401596 <+75>: lea rcx,[rip+0x2ad3] # 0x404070
0x000000000040159d <+82>: call 0x402cc0 <printf>
0x00000000004015a2 <+87>: lea rax,[rbp-0x8]
0x00000000004015a6 <+91>: mov rdx,rax
0x00000000004015a9 <+94>: lea rcx,[rip+0x2ae8] # 0x404098
0x00000000004015b0 <+101>: call 0x402cc0 <printf>
0x00000000004015b5 <+106>: lea rax,[rbp-0xc]
0x00000000004015b9 <+110>: mov rdx,rax
0x00000000004015bc <+113>: lea rcx,[rip+0x2afd] # 0x4040c0
0x00000000004015c3 <+120>: call 0x402cc0 <printf>
0x00000000004015c8 <+125>: lea rax,[rbp-0x20]
0x00000000004015cc <+129>: mov rdx,rax
0x00000000004015cf <+132>: lea rcx,[rip+0x2b12] # 0x4040e8
0x00000000004015d6 <+139>: call 0x402cc0 <printf>
0x00000000004015db <+144>: mov DWORD PTR [rbp-0x4],0x0
0x00000000004015e2 <+151>: jmp 0x401609 <sumset(int, int, int)+190>
0x00000000004015e4 <+153>: mov eax,DWORD PTR [rbp-0x4]
0x00000000004015e7 <+156>: cdqe
0x00000000004015e9 <+158>: mov rax,QWORD PTR [rbp+rax*8-0x20]
0x00000000004015ee <+163>: mov rdx,rax
0x00000000004015f1 <+166>: mov eax,DWORD PTR [rbp-0x4]
0x00000000004015f4 <+169>: mov r8,rdx
0x00000000004015f7 <+172>: mov edx,eax
0x00000000004015f9 <+174>: lea rcx,[rip+0x2b11] # 0x404111
0x0000000000401600 <+181>: call 0x402cc0 <printf>
0x0000000000401605 <+186>: add DWORD PTR [rbp-0x4],0x1
0x0000000000401609 <+190>: mov eax,DWORD PTR [rbp+0x20]
0x000000000040160c <+193>: add eax,0x1
0x000000000040160f <+196>: cmp eax,DWORD PTR [rbp-0x4]
0x0000000000401612 <+199>: jge 0x4015e4 <sumset(int, int, int)+153>
0x0000000000401614 <+201>: lea rdx,[rip+0xffffffffffffff15] # 0x401530 <test()>
0x000000000040161b <+208>: mov eax,DWORD PTR [rbp+0x20]
0x000000000040161e <+211>: cdqe
0x0000000000401620 <+213>: mov rax,QWORD PTR [rbp+rax*8-0x20]
0x0000000000401625 <+218>: mov rcx,rax
0x0000000000401628 <+221>: mov eax,DWORD PTR [rbp+0x20]
0x000000000040162b <+224>: mov r9,rdx
0x000000000040162e <+227>: mov r8,rcx
0x0000000000401631 <+230>: mov edx,eax
0x0000000000401633 <+232>: lea rcx,[rip+0x2aee] # 0x404128
0x000000000040163a <+239>: call 0x402cc0 <printf>
0x000000000040163f <+244>: mov eax,DWORD PTR [rbp+0x20]
0x0000000000401642 <+247>: cdqe
0x0000000000401644 <+249>: lea rdx,[rip+0xfffffffffffffee5] # 0x401530 <test()>
0x000000000040164b <+256>: mov QWORD PTR [rbp+rax*8-0x20],rdx
0x0000000000401650 <+261>: mov edx,DWORD PTR [rbp-0x8]
0x0000000000401653 <+264>: mov eax,DWORD PTR [rbp-0xc]
0x0000000000401656 <+267>: add eax,edx
0x0000000000401658 <+269>: add rsp,0x40
0x000000000040165c <+273>: pop rbp
0x000000000040165d <+274>: ret
好了,我们开始这段痛苦的分析过程
0x000000000040165e <+0>: push rbp
0x000000000040165f <+1>: mov rbp,rsp
0x0000000000401662 <+4>: sub rsp,0x30
0x0000000000401666 <+8>: call 0x4022a0 <__main>
老样子,main函数调用前先进行初始化,这里顺便介绍下上次没有介绍的call指令
CALL(LCALL)指令执行时,进行两步操作:
(1)将程序下一条指令的位置的IP压入堆栈中;
(2)转移到调用的子程序。
0x000000000040166b <+13>: lea rax,[rip+0xffffffffffffffec] # 0x40165e <main()>
0x0000000000401672 <+20>: mov rdx,rax
0x0000000000401675 <+23>: lea rcx,[rip+0x2ad3] # 0x40414f
0x000000000040167c <+30>: call 0x402cc0 <printf>
这次也说一下lea指令,lea指令的全称为Load effect address——取有效地址
我们可以在第一条指令的末尾看到main函数的地址 0x40165e,和前面程序运行结果一致。
这4条指令表示的是代码中main函数中的第一个printf
0x0000000000401681 <+35>: lea rax,[rip+0xfffffffffffffec3] # 0x40154b <sumset(int, int, int)>
0x0000000000401688 <+42>: mov rdx,rax
0x000000000040168b <+45>: lea rcx,[rip+0x2ad9] # 0x40416b
0x0000000000401692 <+52>: call 0x402cc0 <printf>
和前面分析的类似,这里取了sumset函数得到有效地址,表示的是main函数中第二个printf
0x0000000000401697 <+57>: lea rax,[rip+0xfffffffffffffe92] # 0x401530 <test()>
0x000000000040169e <+64>: mov rdx,rax
0x00000000004016a1 <+67>: lea rcx,[rip+0x2adf] # 0x404187
0x00000000004016a8 <+74>: call 0x402cc0 <printf>
main函数中第3个printf
0x00000000004016ad <+79>: lea rcx,[rip+0x2af4] # 0x4041a8
0x00000000004016b4 <+86>: call 0x402cc0 <printf>
while循环中的第一个printf
0x00000000004016b9 <+91>: lea rax,[rbp-0x4]
0x00000000004016bd <+95>: mov rdx,rax
0x00000000004016c0 <+98>: lea rcx,[rip+0x2b00] # 0x4041c7
0x00000000004016c7 <+105>: call 0x402cc8 <scanf>
这里有点奇怪,前面并没有看到rbp-0x4位置的压栈,不过main函数中代码中只有一个int a;不难看出这里是rbp-0x4是a的地址
整段就是scanf语句,输入a的值
0x00000000004016cc <+110>: mov eax,DWORD PTR [rbp-0x4]
0x00000000004016cf <+113>: test eax,eax
0x00000000004016d1 <+115>: jns 0x4016d5 <main()+119>
0x00000000004016d3 <+117>: jmp 0x401707 <main()+169>
将DWORD PTR [rbp-0x4]中的值赋给eax寄存器,即eax=a;
test指令的作用的对2个操作数进行AND操作,并根据运算结果设置相关的标志位。
jns则是如果不为负数,就跳转
这两条语句常常用于if指令中,这里jns的作用是如果a≥0则跳转到0x4016d5的地址上,如果a<0,则执行jmp指令,跳转到0x401707的地址上,查看这两个地址可知,0x4016d5地址上的是下一步操作,0x401707地址已经跳出了循环。
0x00000000004016d5 <+119>: mov ecx,DWORD PTR [rbp-0x4]
0x00000000004016d8 <+122>: mov edx,DWORD PTR [rbp-0x4]
0x00000000004016db <+125>: mov eax,DWORD PTR [rbp-0x4]
0x00000000004016de <+128>: mov r9d,ecx
0x00000000004016e1 <+131>: mov r8d,edx
0x00000000004016e4 <+134>: mov edx,eax
0x00000000004016e6 <+136>: lea rcx,[rip+0x2ae3] # 0x4041d0
0x00000000004016ed <+143>: call 0x402cc0 <printf>
这一大段都是printf,不多作分析
0x00000000004016f2 <+148>: mov ecx,DWORD PTR [rbp-0x4]
0x00000000004016f5 <+151>: mov edx,DWORD PTR [rbp-0x4]
0x00000000004016f8 <+154>: mov eax,DWORD PTR [rbp-0x4]
0x00000000004016fb <+157>: mov r8d,ecx
0x00000000004016fe <+160>: mov ecx,eax
0x0000000000401700 <+162>: call 0x40154b <sumset(int, int, int)>
终于到子函数了,将DWORD PTR [rbp-0x4]的值赋给3个寄存器,再进行寄存器之间的传值,执行call指令,进入sumset函数,并留下返回地址。
0x000000000040154b <+0>: push rbp
0x000000000040154c <+1>: mov rbp,rsp
0x000000000040154f <+4>: sub rsp,0x40
又是初始化,不过,记住这个0x40,后面有用!
0x0000000000401553 <+8>: mov DWORD PTR [rbp+0x10],ecx
0x0000000000401556 <+11>: mov DWORD PTR [rbp+0x18],edx
0x0000000000401559 <+14>: mov DWORD PTR [rbp+0x20],r8d
x,y,i参数的压栈,即形参的实例化,压栈位置不在sumset开辟的空间中,而在main函数的栈帧中
其中i的地址最高,x的地址最低,可见传参顺序为从右到左
0x000000000040155d <+18>: mov eax,DWORD PTR [rbp+0x10]
0x0000000000401560 <+21>: mov DWORD PTR [rbp-0x8],eax
0x0000000000401563 <+24>: mov eax,DWORD PTR [rbp+0x18]
0x0000000000401566 <+27>: mov DWORD PTR [rbp-0xc],eax
前2句指令是将,DWORD PTR [rbp+0x10]的值赋给eax,再进行压栈,即局部变量的创建,对应代码int a = x;
34句同理,对应代码int b = y;
0x0000000000401569 <+30>: lea rax,[rbp+0x10]
0x000000000040156d <+34>: mov rdx,rax
0x0000000000401570 <+37>: lea rcx,[rip+0x2aa9] # 0x404020
0x0000000000401577 <+44>: call 0x402cc0 <printf>
0x000000000040157c <+49>: lea rax,[rbp+0x18]
0x0000000000401580 <+53>: mov rdx,rax
0x0000000000401583 <+56>: lea rcx,[rip+0x2abe] # 0x404048
0x000000000040158a <+63>: call 0x402cc0 <printf>
0x000000000040158f <+68>: lea rax,[rbp+0x20]
0x0000000000401593 <+72>: mov rdx,rax
0x0000000000401596 <+75>: lea rcx,[rip+0x2ad3] # 0x404070
0x000000000040159d <+82>: call 0x402cc0 <printf>
0x00000000004015a2 <+87>: lea rax,[rbp-0x8]
0x00000000004015a6 <+91>: mov rdx,rax
0x00000000004015a9 <+94>: lea rcx,[rip+0x2ae8] # 0x404098
0x00000000004015b0 <+101>: call 0x402cc0 <printf>
0x00000000004015b5 <+106>: lea rax,[rbp-0xc]
0x00000000004015b9 <+110>: mov rdx,rax
0x00000000004015bc <+113>: lea rcx,[rip+0x2afd] # 0x4040c0
0x00000000004015c3 <+120>: call 0x402cc0 <printf>
0x00000000004015c8 <+125>: lea rax,[rbp-0x20]
0x00000000004015cc <+129>: mov rdx,rax
0x00000000004015cf <+132>: lea rcx,[rip+0x2b12] # 0x4040e8
0x00000000004015d6 <+139>: call 0x402cc0 <printf>
6个printf,不讲了
0x00000000004015db <+144>: mov DWORD PTR [rbp-0x4],0x0
0x00000000004015e2 <+151>: jmp 0x401609 <sumset(int, int, int)+190>
同样是局部变量的创建,这里是int k = 0;同时跳转到0x401609
0x0000000000401609 <+190>: mov eax,DWORD PTR [rbp+0x20]
0x000000000040160c <+193>: add eax,0x1
0x000000000040160f <+196>: cmp eax,DWORD PTR [rbp-0x4]
0x0000000000401612 <+199>: jge 0x4015e4 <sumset(int, int, int)+153>
我们来到401609地址,前两句指令,将DWORD PTR [rbp+0x20]的值赋给寄存器eax,再将eax加1,rbp+0x20对应的是形参i
所以这两句是for循环中的 i+1;
接下去的2句,比较eax和DWORD PTR [rbp-0x4]的值,如果DWORD PTR [rbp-0x4]小于或等于eax的值,进行跳转,否则继续下一条指令,即跳出for循环,这里表示的是如果k≤i+1,则继续for循环,否则跳出
0x00000000004015e4 <+153>: mov eax,DWORD PTR [rbp-0x4]
0x00000000004015e7 <+156>: cdqe
0x00000000004015e9 <+158>: mov rax,QWORD PTR [rbp+rax*8-0x20]
0x00000000004015ee <+163>: mov rdx,rax
0x00000000004015f1 <+166>: mov eax,DWORD PTR [rbp-0x4]
0x00000000004015f4 <+169>: mov r8,rdx
0x00000000004015f7 <+172>: mov edx,eax
0x00000000004015f9 <+174>: lea rcx,[rip+0x2b11] # 0x404111
0x0000000000401600 <+181>: call 0x402cc0 <printf>
0x0000000000401605 <+186>: add DWORD PTR [rbp-0x4],0x1
跳转来到0x4015e4,执行for函数中的printf,接着k++,继续循环判断
0x0000000000401614 <+201>: lea rdx,[rip+0xffffffffffffff15] # 0x401530 <test()>
0x000000000040161b <+208>: mov eax,DWORD PTR [rbp+0x20]
0x000000000040161e <+211>: cdqe
0x0000000000401620 <+213>: mov rax,QWORD PTR [rbp+rax*8-0x20]
0x0000000000401625 <+218>: mov rcx,rax
0x0000000000401628 <+221>: mov eax,DWORD PTR [rbp+0x20]
0x000000000040162b <+224>: mov r9,rdx
0x000000000040162e <+227>: mov r8,rcx
0x0000000000401631 <+230>: mov edx,eax
0x0000000000401633 <+232>: lea rcx,[rip+0x2aee] # 0x404128
0x000000000040163a <+239>: call 0x402cc0 <printf>
最后一个printf
0x000000000040163f <+244>: mov eax,DWORD PTR [rbp+0x20]
0x0000000000401642 <+247>: cdqe
0x0000000000401644 <+249>: lea rdx,[rip+0xfffffffffffffee5] # 0x401530 <test()>
0x000000000040164b <+256>: mov QWORD PTR [rbp+rax*8-0x20],rdx
0x0000000000401650 <+261>: mov edx,DWORD PTR [rbp-0x8]
0x0000000000401653 <+264>: mov eax,DWORD PTR [rbp-0xc]
0x0000000000401656 <+267>: add eax,edx
0x0000000000401658 <+269>: add rsp,0x40
0x000000000040165c <+273>: pop rbp
0x000000000040165d <+274>: ret
第一条指令,取出DWORD PTR [rbp+0x20]中的值放入寄存器eax中,即eax=i;
接着,lea指令,取test的地址放入rdx寄存器中
QWORD PTR [rbp+rax*8-0x20]对应的是c[i],即将test的地址赋给c[i];
最后几句是取出a和b的值,相加并返回。
好了,我们整个过程就先分析到这里,但是,我们仍然没有解决前面的2个问题,为什么输入4后再输入会导致程序停止,为什么输入5会调用test函数
先说明一下void* 指针是8个字节,unsigned long long也是8个字节,观察sumset的汇编代码,没有看到c的压栈过程,但是,有一段QWORD PTR [rbp+rax*8-0x20],0x20即十进制的32,rbp-0x20即开辟了32个字节的空间,虽然在sumset函数中, void* c[1]只定义了一位的数组,但是实际开辟的空间却足够放32/8=4个,所以在产生越界数组c[1]-c[3]时没有发生任何问题,程序仍正常进行。
但是,当达到c[4]时,没有剩余空间给他了,那怎么办呢,根据上面的程序,我们可以得到c[4] = 62fe50,这是什么地址,在调试器中查看得到,这是main函数的基址寄存器rbp的地址,之后test函数的地址赋值给c[4],即改变main函数基址寄存器的地址为test函数的地址,sumset函数调用结束返回,重新开始while循环,当运行到scanf时,遇到了指令
0x00000000004016b9 <+91>: lea rax,[rbp-0x4]
此时,rbp地址被改变了,rbp-0x4是一个不属于这个程序的存储地址,被操作系统捕获到,然后中止了程序运行。
接着是输入5,查看可得c[5] = 401705,汇编代码中,401705对应的指令如下
0x0000000000401705 <+167>: jmp 0x4016ad <main()+79>
这是while循环的跳转指令,也是sumset函数的返回地址,test函数的地址赋值给c[5],即将返回地址修改为test函数的地址,在sumset函数执行完直接返回到test函数,但test函数不是被正常调用的,没有进行初始化等操作,所以test函数的return会返回到不属于这个程序的存储地址去,从而被操作系统捕获到,然后中止了程序运行。
void* c[1]开辟的是32位的空间,那void* c[2],void* c[3]呢?修改代码查看调试器得当void* c[i],i的值为1-4时,开辟的空间都为32,当i的值为5-8时,开辟的空间都为64(总开辟空间也随之增加为0x60,即96),可以存放8个;所以当代码为void* c[5]时,输入8和9才能导致返回地址改变