c++ 代码块的局部成员栈布局
背景:
c++ 代码块是非常常用的方式, 例如if 分支,循环分支,还有些加锁的时候需要,锁的力度比较小的时候,都需要代码块,理解函数栈对你理解协程也有一定的帮助,可以说代码块在c++开发的各个方面,但是c++代码块的局部成员,在栈中怎么布局的,一直比较模糊,本次从win32,arm64不同的平台下都去看下代码块的栈 布局
测试代码
首先看下测试代码
C++ int TestStackA() { int a = 0x55; int b = 0xaa; int c = 0xcc; int d = 0xee; int e = 0x11; return 0x1234; }
这个代码段很简单,没有代码块,我们转成汇编看下
C++ 00A315F0 push ebp 00A315F1 mov ebp,esp 00A315F3 sub esp,0FCh;分配栈空间 00A315F9 push ebx 00A315FA push esi 00A315FB push edi 00A315FC lea edi,[ebp+FFFFFF04h] 00A31602 mov ecx,3Fh 00A31607 mov eax,0CCCCCCCCh ;把分配的栈空间设置成cccccccc 00A3160C rep stos dword ptr es:[edi] 00A3160E mov dword ptr [ebp-8],55h ;a 00A31615 mov dword ptr [ebp-14h],0AAh ;b 00A3161C mov dword ptr [ebp-20h],0CCh ;c 00A31623 mov dword ptr [ebp-2Ch],0EEh ;d 00A3162A mov dword ptr [ebp-38h],11h ;e 00A31631 mov eax,1234h ;返回值 00A31636 pop edi 00A31637 pop esi 00A31638 pop ebx 00A31639 mov esp,ebp 00A3163B pop ebp 00A3163C ret
这段汇编很简单,接下来我们对比TestB代码块的汇编
C++ int TestStackB() { int a = 0x55; int b = 0xaa; { int c = 0xcc; { int d = 0xee; } } int e = 0x11; return 0x1234; }
C++ 00A31640 push ebp 00A31641 mov ebp,esp 00A31643 sub esp,0FCh 00A31649 push ebx 00A3164A push esi 00A3164B push edi 00A3164C lea edi,[ebp+FFFFFF04h] 00A31652 mov ecx,3Fh 00A31657 mov eax,0CCCCCCCCh 00A3165C rep stos dword ptr es:[edi] 00A3165E mov dword ptr [ebp-8],55h 00A31665 mov dword ptr [ebp-14h],0AAh 00A3166C mov dword ptr [ebp-20h],0CCh 00A31673 mov dword ptr [ebp-2Ch],0EEh 00A3167A mov dword ptr [ebp-38h],11h 00A31681 mov eax,1234h 00A31686 pop edi 00A31687 pop esi 00A31688 pop ebx 00A31689 mov esp,ebp 00A3168B pop ebp 00A3168C ret
你会惊奇的发现栈空间布局是一摸一样的,也就是说块代码的成员变量,在函数进来的分配栈空间的时候就已经分配好了
接下来我们看下带析构方法的栈空间是什么样的
C++ int TestStackA() { int a = 0x55; int b = 0xaa; int c = 0xcc; std::string str; int d = 0xee; int e = 0x11; return 0x1234; }
C++ 00BC15F0 push ebp 00BC15F1 mov ebp,esp 00BC15F3 sub esp,130h ....... 00BC1615 mov dword ptr [ebp-4],eax 00BC1618 mov dword ptr [ebp-0Ch],55h 00BC161F mov dword ptr [ebp-18h],0AAh 00BC1626 mov dword ptr [ebp-24h],0CCh 00BC162D lea ecx,[ebp-48h] 00BC1630 call 00BA6640 ; 调用str构造方法 00BC1635 mov dword ptr [ebp-54h],0EEh 00BC163C mov dword ptr [ebp-60h],11h ;int e 00BC1643 mov dword ptr [ebp+FFFFFED4h],1234h 00BC164D lea ecx,[ebp-48h] 00BC1650 call 00B70A80 ; 调用str的析构方法 00BC1655 mov eax,dword ptr [ebp+FFFFFED4h] 00BC165B push edx 00BC165C mov ecx,ebp 00BC165E push eax 00BC165F lea edx,ds:[00BC168Ch] 00BC1665 call 017FDCE0
可以看出来的str析构方法是在int e = 0x11;赋值语句之后的,接下来我们看下另外代码块的str具体汇编
C++ int TestStackB() { int a = 0x55; int b = 0xaa; { int c = 0xcc; { std::string str; int d = 0xee; } } int e = 0x11; return 0x1234; }
C++ 00BC16B0 push ebp 00BC16B1 mov ebp,esp 00BC16B3 ...... 00BC16D5 mov dword ptr [ebp-4],eax 00BC16D8 mov dword ptr [ebp-0Ch],55h 00BC16DF mov dword ptr [ebp-18h],0AAh 00BC16E6 mov dword ptr [ebp-24h],0CCh 00BC16ED lea ecx,[ebp-48h] 00BC16F0 call 00BA6640 ; 调用str构造方法 00BC16F5 mov dword ptr [ebp-54h],0EEh 00BC16FC lea ecx,[ebp-48h] 00BC16FF call 00B70A80 ; 调用str析构方法 00BC1704 mov dword ptr [ebp-60h],11h ;int e 00BC170B mov eax,1234h 00BC1710 push edx 00BC1711 mov ecx,ebp 00BC1713 push eax
可以看的出来 str析构方法在是int e之前调用的,也就是说出作用于调用析构方法,完全是编译器编译的发生的,而真正的内存在函数栈分配的时候就已经有了
接下来我们看下arm64的栈空间是什么样的
c代码还是上边的
TestStackA的arm64汇编
C++ 0x100001408 <+0>: sub sp, sp, #0x20 0x10000140c <+4>: mov w8, #0x55 0x100001410 <+8>: str w8, [sp, #0x1c] 0x100001414 <+12>: mov w8, #0xaa 0x100001418 <+16>: str w8, [sp, #0x18] 0x10000141c <+20>: mov w8, #0xcc 0x100001420 <+24>: str w8, [sp, #0x14] 0x100001424 <+28>: mov w8, #0xee 0x100001428 <+32>: str w8, [sp, #0x10] 0x10000142c <+36>: mov w8, #0x11 -> 0x100001430 <+40>: str w8, [sp, #0xc] 0x100001434 <+44>: mov w0, #0x1234 0x100001438 <+48>: add sp, sp, #0x20 0x10000143c <+52>: ret
TestStackB的arm64汇编
C++ 0x100001440 <+0>: sub sp, sp, #0x20 0x100001444 <+4>: mov w8, #0x55 0x100001448 <+8>: str w8, [sp, #0x1c] 0x10000144c <+12>: mov w8, #0xaa 0x100001450 <+16>: str w8, [sp, #0x18] 0x100001454 <+20>: mov w8, #0xcc 0x100001458 <+24>: str w8, [sp, #0x14] 0x10000145c <+28>: mov w8, #0xee 0x100001460 <+32>: str w8, [sp, #0x10] 0x100001464 <+36>: mov w8, #0x11 -> 0x100001468 <+40>: str w8, [sp, #0xc] 0x10000146c <+44>: mov w0, #0x1234 0x100001470 <+48>: add sp, sp, #0x20 0x100001474 <+52>: ret
可以看的出来一模一样
我们在看下 带str的汇编
C++ test`TestStackA: 0x1000052d8 <+0>: sub sp, sp, #0x50 0x1000052dc <+4>: stp x29, x30, [sp, #0x40] 0x1000052e0 <+8>: add x29, sp, #0x40 0x1000052e4 <+12>: mov w8, #0x55 0x1000052e8 <+16>: stur w8, [x29, #-0x4] 0x1000052ec <+20>: mov w8, #0xaa 0x1000052f0 <+24>: stur w8, [x29, #-0x8] 0x1000052f4 <+28>: mov w8, #0xcc 0x1000052f8 <+32>: stur w8, [x29, #-0xc] 0x1000052fc <+36>: mov w8, #0xee 0x100005300 <+40>: stur w8, [x29, #-0x10] 0x100005304 <+44>: add x0, sp, #0x18 0x100005308 <+48>: str x0, [sp, #0x8] 0x10000530c <+52>: bl 0x100005330 ; std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::basic_string at string:1832 0x100005310 <+56>: ldr x0, [sp, #0x8] 0x100005314 <+60>: mov w8, #0x11;int e -> 0x100005318 <+64>: str w8, [sp, #0x14] 0x10000531c <+68>: bl 0x100007b18 ; symbol stub for: std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::~basic_string() 0x100005320 <+72>: mov w0, #0x1234 0x100005324 <+76>: ldp x29, x30, [sp, #0x40] 0x100005328 <+80>: add sp, sp, #0x50 0x10000532c <+84>: ret
可以看的出来str析构方法是在mov w8, #0x11 也就是int e之后
C++ test`TestStackB: 0x10000535c <+0>: sub sp, sp, #0x50 0x100005360 <+4>: stp x29, x30, [sp, #0x40] 0x100005364 <+8>: add x29, sp, #0x40 0x100005368 <+12>: mov w8, #0x55 0x10000536c <+16>: stur w8, [x29, #-0x4] 0x100005370 <+20>: mov w8, #0xaa 0x100005374 <+24>: stur w8, [x29, #-0x8] 0x100005378 <+28>: mov w8, #0xcc 0x10000537c <+32>: stur w8, [x29, #-0xc] 0x100005380 <+36>: add x0, sp, #0x18 0x100005384 <+40>: str x0, [sp, #0x8] 0x100005388 <+44>: bl 0x100005330 ; std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::basic_string at string:1832 0x10000538c <+48>: ldr x0, [sp, #0x8] 0x100005390 <+52>: mov w8, #0xee 0x100005394 <+56>: str w8, [sp, #0x14] 0x100005398 <+60>: bl 0x100007b18 ; symbol stub for: std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::~basic_string() 0x10000539c <+64>: mov w8, #0x11;int e -> 0x1000053a0 <+68>: str w8, [sp, #0x10] 0x1000053a4 <+72>: mov w0, #0x1234 0x1000053a8 <+76>: ldp x29, x30, [sp, #0x40] 0x1000053ac <+80>: add sp, sp, #0x50 0x1000053b0 <+84>: ret
可以看的出来str析构方法是在mov w8, #0x11 也就是int e之前
总结:
arm64 做法跟win32基本是一致的,代码块里的局部成员的空间在函数进来的时候,分配函数栈空间的时候就已经分配出来的, 代码块的成员析构方法在出作用域的时候会调用。