当前位置: 首页 > news >正文

美文的手机网站上海自聊自做网站

美文的手机网站,上海自聊自做网站,湖北二师网站建设排名,网站优化排名易下拉效率结构体 结构体对齐 设置结构体对齐值 方法1#xff1a;在 Visual Studio 中可以在 项目属性 - 配置属性 - C/C - 所有选项 - 结构体成员对齐 中设置结构体对齐大小。方法2#xff1a;使用 #pragma pack(对齐值) 来设置#xff0c;不过要想单独设置一个结…结构体 结构体对齐 设置结构体对齐值 方法1在 Visual Studio 中可以在 项目属性 - 配置属性 - C/C - 所有选项 - 结构体成员对齐 中设置结构体对齐大小。方法2使用 #pragma pack(对齐值) 来设置不过要想单独设置一个结构体的对齐大小需要保存和恢复原先的结构体对齐值。#pragma pack(push) // 保存原先的结构体对齐值 #pragma pack(2) // 设置结构体对齐值为 2 struct Struct { // sizeof(Struct) 6char x;int y; }; #pragma pack(pop) // 恢复原先的结构体对齐值方法3在 C11 及以后标准中可使用 alians 关键字设置结构体的对齐值。不过请注意alignas 关键字的参数必须是常量表达式对齐值必须是 2 的幂且不能小于结构体中最大的成员。struct alignas(32) Struct { // sizeof(Struct) 32char x;int y; };结构体对齐策略 假设一个结构体中有 n n n 个元素每个元素大小为 a i ( 1 ≤ i ≤ n ) a_i(1\le i\le n) ai​(1≤i≤n) 并且按照 k k k 字节对齐则结构体大小计算方式如下 #include bits/stdc.hint main() {std::ios::sync_with_stdio(false);std::cin.tie(nullptr);int n, k;std::cin n k;assert(__builtin_popcount(k) 1);std::vectorint a(n);for (int i 0; i n; i) {std::cin a[i];assert(__builtin_popcount(a[i]) 1);}k std::min(k, *std::max_element(a.begin(), a.end()));int ans 0;for (int i 0; i n; i) {if ((ans a[i] - 1) / a[i] * a[i] a[i] (ans k - 1) / k * k) {ans (ans a[i] - 1) / a[i] * a[i] a[i];} else {ans (ans k - 1) / k * k a[i];}}ans (ans k - 1) / k * k;std::cout ans std::endl;return 0; }注意以下特殊情况 如果是 alignas 设置的对齐值则结构体严格按照对齐值对齐IDA 中设置的结构体 align 属性也按照这个规则对齐结构体否则按照对齐值和结构体最大成员中最小的那个进行对齐。如果是结构体套结构体则内部的结构体的成员需要看做是外部结构体的成员进行对齐而不是内部的结构体整个参与到结构体对齐中去。 结构体的识别 通常采用 [base offset] 的方式访问结构体成员。 如果结构体中成员大小相同则结构体初始化代码等价与数组的初始化代码无法区分。如果结构体中成员的大小或类型整型与浮点数不同会造成结构体成员在内存中不连续或者访问的汇编指令不同可以识别出结构体。如果采用 [esp xxx] 或者 [ebp - xxx] 访问则不能区分是结构体还是多个局部变量。 结构体拷贝 如果结构体比较小则利用寄存器进行拷贝。 Struct b *a; 006B186C mov eax,dword ptr [a] 006B186F mov ecx,dword ptr [eax] 006B1871 mov dword ptr [b],ecx 006B1874 mov edx,dword ptr [eax4] 006B1877 mov dword ptr [ebp-18h],edx ; [ebp - 18h] 为 [b 4] 006B187A mov eax,dword ptr [eax8] 006B187D mov dword ptr [ebp-14h],eax ; [ebp - 14h] 为 [b 8] 如果结构体比较大则优化为 rep 指令。 Struct b *a; 00F8186C mov ecx,0Ch 00F81871 mov esi,dword ptr [a] 00F81874 lea edi,[b] 00F81877 rep movs dword ptr es:[edi],dword ptr [esi]结构体传参 例如下面这个代码 #include iostreamstruct Struct {int x;int y; };void foo(Struct a) {printf(%d %d\n, a.x, a.y); }int main() {Struct a;scanf_s(%d%d, a.x, a.y);foo(a); }在结构体成员比较少的情况下调用 foo 函数时会依次将结构体成员 push 到栈上。类似于函数正常传参。 foo(a); 007C45E4 mov eax,dword ptr [ebp-0Ch] ; [ebp - 0Ch] 为 [a 4] 007C45E7 push eax 007C45E8 mov ecx,dword ptr [a] 007C45EB push ecx 007C45EC call foo (07C13CFh) 007C45F1 add esp,8 将 Struct 修改为如下定义 struct Struct {int x;int y;int z[10]; };则 foo 函数通过 rep 指令栈拷贝传参而如果是数组传参则会传数组的地址这是区分数组和结构体的一个依据。 foo(a); 005345E4 sub esp,30h 005345E7 mov ecx,0Ch 005345EC lea esi,[a] 005345EF mov edi,esp 005345F1 rep movs dword ptr es:[edi],dword ptr [esi] 005345F3 call foo (05313CFh) 005345F8 add esp,30h 如果传入的参数是结构体引用或是结构体指针则于数组参数一样传的是结构体的地址这样就只能根据函数中对结构体成员访问来判断参数类型是否是结构体。 foo(a); // a 是一个结构体引用 006017F8 lea eax,[a] 006017FB push eax 006017FC call foo (060105Fh) 00601801 add esp,4 结构体返回值 首先让结构体只有一个成员变量 #include iostreamstruct Struct {int x; };Struct bar() {Struct a;printf(%d\n, a.x);return a; }int main() {Struct a bar();printf(%d\n, a.x);return 0; }此时会将结构体存放在 eax 寄存器中返回。 Struct a bar(); 00AC1B93 call bar (0AC10D2h) 00AC1B98 mov dword ptr [ebp-48h],eax 00AC1B9B mov eax,dword ptr [ebp-48h] 00AC1B9E mov dword ptr [a],eax 将结构体中添加一个成员变量 y 。 struct Struct {int x, y; };此时返回值结构体中的两个成员变量分别使用 eax 和 edx 寄存器存储。这与 32 位下返回 64 位变量相似。 Struct a bar(); 009A1B93 call bar (09A10D2h) 009A1B98 mov dword ptr [ebp-50h],eax 009A1B9B mov dword ptr [ebp-4Ch],edx 009A1B9E mov eax,dword ptr [ebp-50h] 009A1BA1 mov ecx,dword ptr [ebp-4Ch] 009A1BA4 mov dword ptr [a],eax 009A1BA7 mov dword ptr [ebp-4],ecx 因此结构体大小不超过 8 字节的时候采用值返回。 将结构体中再添加一个成员变量 z 。 struct Struct {int x, y, z; };此时不再使用寄存器存返回值而是向函数中传一个 ebp - 0x24 的地址作为参数。 bar 函数返回后先将返回值 eax 指向的 12 字节内存拷贝到 ebp - 0x0C 处的内存之后再将 ebp - 0x0C 处的内存拷贝到 ebp -0x18 也就是局部变量 b 所在的内存。 Struct b bar(); .text:00401146 lea eax, [ebpa] .text:00401149 push eax ; a .text:0040114A call ?barYA?AUStructXZ ; bar(void) .text:0040114A .text:0040114F add esp, 4 .text:00401152 mov ecx, [eaxStruct.x] .text:00401154 mov [ebptemp.x], ecx .text:00401157 mov edx, [eaxStruct.y] .text:0040115A mov [ebptemp.y], edx .text:0040115D mov eax, [eaxStruct.z] .text:00401160 mov [ebptemp.z], eax .text:00401163 mov ecx, [ebptemp.x] .text:00401166 mov [ebpb.x], ecx .text:00401169 mov edx, [ebptemp.y] .text:0040116C mov [ebpb.y], edx .text:0040116F mov eax, [ebptemp.z] .text:00401172 mov [ebpb.z], eax传入的参数在 bar 函数中被看做是局部变量 a 。 Struct *__cdecl bar(Struct *a) {a-x 1;a-y 0;a-z 0;scanf_s(%d%d%d\n, a, a-y, a-z);return a; }因此整个过程中发生 2 次结构体拷贝。 如果 bar 函数本身还传参则结构体局部变量 a地址作为第一个参数。 Struct b bar(x); 00761161 mov ecx,dword ptr [x] 00761164 push ecx 00761165 lea edx,[ebp-2Ch] 00761168 push edx 00761169 call bar (0761100h) 0076116E add esp,8 将结构体定义的再大一些此时同样会发生 2 次拷贝不过会使用 rep 指令进行优化。 Struct b bar(); 00A8114B lea eax,[ebp-90h] 00A81151 push eax 00A81152 call bar (0A81100h) 00A81157 add esp,4 00A8115A mov ecx,0Ch 00A8115F mov esi,eax 00A81161 lea edi,[ebp-30h] 00A81164 rep movs dword ptr es:[edi],dword ptr [esi] 00A81166 mov ecx,0Ch 00A8116B lea esi,[ebp-30h] 00A8116E lea edi,[b] 00A81171 rep movs dword ptr es:[edi],dword ptr [esi] 成员函数 普通成员函数 调用约定是 __thiscall即 ecx 寄存器存 this 指针内平栈。 a.f(1, 2, 3); 008A1050 push 3 008A1052 push 2 008A1054 push 1 008A1056 lea ecx,[a] 008A1059 call Struct::f (08A1010h) 在不开优化的前提下如果使用 __stdcall 修饰成员函数则 this 指针作为第一个参数栈传参。 a.f(1, 2, 3); 000C1050 push 3 000C1052 push 2 000C1054 push 1 000C1056 lea eax,[a] 000C1059 push eax 000C105A call Struct::f (0C1010h) 同理使用 __fastcall 和 __cdecl 修饰成员函数后成员函数满足对应的调用约定只不过 this 指针当做函数的第一个参数。 构造函数 栈对象 栈对象即 Class 在栈上实例化出的 Object 。 构造函数是对应类的作用域中第一个被调用的成员函数调用约定是 __thiscall 。 Class a Class(); 007C1128 lea ecx,[a] 007C112B call Class::Class (07C10C0h)构造函数的返回值是 this 指针。 Class() { 007C10C0 push ebp 007C10C1 mov ebp,esp 007C10C3 push ecx 007C10C4 mov dword ptr [this],ecx puts(Construct); 007C10C7 push offset string Construct (07C2120h) 007C10CC call dword ptr [__imp__puts (07C20BCh)] 007C10D2 add esp,4 } 007C10D5 mov eax,dword ptr [this] 007C10D8 mov esp,ebp 007C10DA pop ebp 007C10DB ret 构造函数反编译代码如下 Class *__thiscall Class::Class(Class *this) {_puts(Construct);return this; }堆对象 堆对象即 Class 在堆上实例化出的 Object 。由于堆对象没有作用域概念因此构造函数是在 new 之后编译器自动添加调用的同理析构函数是在 delete 前调用的。 Class* anew Class(); 00EC1065 push 30h 00EC1067 call operator new (0EC111Ch) ; new 一块 0x30 大小的内存 00EC106C add esp,4 00EC106F mov dword ptr [ebp-10h],eax ; 把 new 到的指针给一个临时变量 temp 00EC1072 mov dword ptr [ebp-4],0 ; [ebp-4] 是 TryLevel实际上调用构造函数的代码外面包着一层异常处理。 00EC1079 cmp dword ptr [ebp-10h],0 ; 判断内存是否分配成功如果分配失败则跳过构造函数。 00EC107D je main4Ch (0EC108Ch) 00EC107F mov ecx,dword ptr [ebp-10h] ; 取出 [ebp-10h] 存放的 Object 地址作为 this 指针 temp 00EC1082 call Class::Class (0EC1000h) ; 调用构造函数 00EC1087 mov dword ptr [ebp-14h],eax 00EC108A jmp main53h (0EC1093h) 00EC108C mov dword ptr [ebp-14h],0 ; 如果 new 分配内存失败会将存放构造函数返回值的栈上局部变量置 0 表示没有调用构造函数 00EC1093 mov eax,dword ptr [ebp-14h] 00EC1096 mov dword ptr [ebp-1Ch],eax 00EC1099 mov dword ptr [ebp-4],0FFFFFFFFh ; TryLevel 置为 -1 表示已经不在 try...catch... 范围了。 00EC10A0 mov ecx,dword ptr [ebp-1Ch] 00EC10A3 mov dword ptr [a],ecx ; [ebp-14h] - eax - [ebp-1Ch] - ecx - [a]不考虑异常处理可反编译成如下 C 代码 int __cdecl main(int argc, const char **argv, const char **envp) {Class *a; // [esp14h] [ebp-14h]Class *temp; // [esp18h] [ebp-10h]temp (Class *)operator new(0x30u);if ( temp )a Class::Class(temp);elsea 0;if ( a )Class::scalar deleting destructor(a, 1u);return 0; }全局对象静态对象 以动态链接程序为例在程序启动后有如下调用链mainCRTStartup - _scrt_common_main_seh 。 在 _scrt_common_main_seh 函数中有如 _initterm_e 和 _initterm 函数这个两个函数分别是 C 和 C 的初始化函数。以 _initterm 函数为例这个函数会依次调用 __xc_a 和 __xc_z 之间的函数指针。 __scrt_current_native_startup_state initializing;if ( _initterm_e(__xi_a, __xi_z) )return 255;_initterm(__xc_a, __xc_z);__scrt_current_native_startup_state initialized;我们看到再这些函数指针中有一个 _dynamic_initializer_for__a__ 函数这个函数主要做了两件事 调用全局对象的构造函数调用 _atexit 注册 _dynamic_atexit_destructor_for__a__ 函数以便在程序结束的时候调用该函数析构全局对象。 ; int dynamic_initializer_for__a__() _dynamic_initializer_for__a__ proc near push ebp mov ebp, esp mov ecx, offset ?a3VClassA ; this call ??0ClassQAEXZ ; Class::Class(void) push offset _dynamic_atexit_destructor_for__a__ ; function call _atexit add esp, 4 pop ebp retn _dynamic_initializer_for__a__ endp反编译代码如下 int dynamic_initializer_for__a__() {Class::Class(a);return atexit(dynamic_atexit_destructor_for__a__); }对象数组 如果定义一个栈上对象数组则程序会依次调用数组中每个对象的构造函数而 Visual C 编译器会将这一过程定义为一个函数 eh vector constructor iterator push offset ??1ClassQAEXZ ; destructor push offset ??0ClassQAEXZ ; constructor push 0Ah ; count push 30h ; 0 ; size lea eax, [ebpa] push eax ; ptr call ??_LYGXPAXIIP6EX0Z1Z ; eh vector constructor iterator(void *,uint,uint,void (*)(void *),void (*)(void *))反编译代码如下 int __cdecl main(int argc, const char **argv, const char **envp) {Class a[10]; // [esp4h] [ebp-1E4h] BYREFeh vector constructor iterator(a,0x30u,0xAu,(void (__thiscall *)(void *))Class::Class,(void (__thiscall *)(void *))Class::~Class);eh vector destructor iterator(a, 0x30u, 0xAu, (void (__thiscall *)(void *))Class::~Class);return 0; }eh vector constructor iterator 的参数分别是 对象数组的首地址对象的大小对象的数量构造函数地址析构函数地址 该函数的会依次为每个对象调用构造函数。 void __stdcall eh vector constructor iterator(char *ptr,unsigned int size,unsigned int count,void (__thiscall *constructor)(void *),void (__thiscall *destructor)(void *)) {int i; // ebxfor ( i 0; i ! count; i ){constructor(ptr);ptr size;} }从 IDA 反编译结果来看 destructor 函数指针没有用到但实际上这里有一个异常处理即构造出现异常时会调用 __ArrayUnwind 函数将已初始化的对象析构。 .text:004011C3 ; void __stdcall eh vector constructor iterator(char *ptr, unsigned int size, unsigned int count, void (__thiscall *constructor)(void *), void (__thiscall *destructor)(void *)) .text:004011C3 ??_LYGXPAXIIP6EX0Z1Z proc near ; CODE XREF: _main28↑p .text:004011C3 .text:004011C3 i dword ptr -20h .text:004011C3 success byte ptr -19h .text:004011C3 ms_exc CPPEH_RECORD ptr -18h .text:004011C3 ptr dword ptr 8 .text:004011C3 size dword ptr 0Ch .text:004011C3 count dword ptr 10h .text:004011C3 constructor dword ptr 14h .text:004011C3 destructor dword ptr 18h .text:004011C3 .text:004011C3 ; __unwind { // __SEH_prolog4 .text:004011C3 push 10h .text:004011C5 push offset ScopeTable .text:004011CA call __SEH_prolog4 .text:004011CA .text:004011CF xor ebx, ebx .text:004011D1 mov [ebpi], ebx .text:004011D4 mov [ebpsuccess], bl ; 初始化局部变量 success 为 false .text:004011D7 ; __try { // __finally(HandlerFunc) .text:004011D7 mov [ebpms_exc.registration.TryLevel], ebx .text:004011D7 .text:004011DA .text:004011DA LOOP: ; CODE XREF: eh vector constructor iterator(void *,uint,uint,void (*)(void *),void (*)(void *))35↓j .text:004011DA cmp ebx, [ebpcount] .text:004011DD jz short SUCCESS .text:004011DD .text:004011DF mov ecx, [ebpconstructor] ; Target .text:004011E2 call ds:___guard_check_icall_fptr ; _guard_check_icall_nop(x) .text:004011E2 .text:004011E8 mov ecx, [ebpptr] ; void * .text:004011EB call [ebpconstructor] .text:004011EB .text:004011EE mov eax, [ebpsize] .text:004011F1 add [ebpptr], eax .text:004011F4 inc ebx .text:004011F5 mov [ebpi], ebx .text:004011F8 jmp short LOOP .text:004011F8 .text:004011FA ; --------------------------------------------------------------------------- .text:004011FA .text:004011FA SUCCESS: ; CODE XREF: eh vector constructor iterator(void *,uint,uint,void (*)(void *),void (*)(void *))1A↑j .text:004011FA mov al, 1 ; 更新 al 寄存器和局部变量 success 为 true .text:004011FC mov [ebpsuccess], al .text:004011FC ; } // starts at 4011D7 .text:004011FF mov [ebpms_exc.registration.TryLevel], 0FFFFFFFEh .text:00401206 call HandleIfFailure ; 调用 .text:00401206 .text:0040120B ; --------------------------------------------------------------------------- .text:0040120B .text:0040120B END: ; CODE XREF: eh vector constructor iterator(void *,uint,uint,void (*)(void *),void (*)(void *)):RETN↓j .text:0040120B mov ecx, [ebpms_exc.registration.Next] .text:0040120E mov large fs:0, ecx .text:00401215 pop ecx .text:00401216 pop edi .text:00401217 pop esi .text:00401218 pop ebx .text:00401219 leave .text:0040121A retn 14h .text:0040121A .text:0040121D ; --------------------------------------------------------------------------- .text:0040121D .text:0040121D HandlerFunc: ; DATA XREF: .rdata:ScopeTable↓o .text:0040121D ; __finally // owned by 4011D7 ; 设置 ebp 为 i这是需要调用析构函数的对象的数量。 .text:0040121D mov ebx, [ebpi] .text:00401220 mov al, [ebpsuccess] ; 设置 al 为 局部变量 success 即 false .text:00401220 .text:00401223 .text:00401223 HandleIfFailure: ; CODE XREF: eh vector constructor iterator(void *,uint,uint,void (*)(void *),void (*)(void *))43↑j .text:00401223 test al, al .text:00401225 jnz short RETN ; 如果 al 为 true 则直接返回 .text:00401225 .text:00401227 push [ebpdestructor] ; destructor .text:0040122A push ebx ; count .text:0040122B push [ebpsize] ; size .text:0040122E push [ebpptr] ; ptr .text:00401231 call ?__ArrayUnwindYGXPAXIIP6EX0ZZ ; 否则调用 __ArrayUnwind 函数将已初始化的对象析构 .text:00401231 .text:00401236 .text:00401236 RETN: ; CODE XREF: eh vector constructor iterator(void *,uint,uint,void (*)(void *),void (*)(void *))62↑j .text:00401236 retn .text:00401236 ; } // starts at 4011C3 .text:00401236 .text:00401236 ??_LYGXPAXIIP6EX0Z1Z endp对于堆上对象数组也是调用 eh vector constructor iterator 函数构造大致逻辑如下因为是数组所以申请的内存的前 4 字节用来记录数组中成员的个数。 int __cdecl main(int argc, const char **argv, const char **envp) {Class *v4; // [esp14h] [ebp-14h]_DWORD *block; // [esp18h] [ebp-10h]block operator new[](0x1E4u);if ( block ){*block 10;eh vector constructor iterator(block 1,0x30u,0xAu,(void (__thiscall *)(void *))Class::Class,(void (__thiscall *)(void *))Class::~Class);v4 (Class *)(block 1);}else{v4 0;}if ( v4 )Class::vector deleting destructor(v4, 3u);return 0; }对于全局对象数组则是在 dynamic_initializer_for__a__ 函数中调用 eh vector constructor iterator 函数。 int dynamic_initializer_for__a__() {eh vector constructor iterator(a,0x30u,0xAu,(void (__thiscall *)(void *))Class::Class,(void (__thiscall *)(void *))Class::~Class);return atexit(dynamic_atexit_destructor_for__a__); }析构函数 栈对象 栈对象有如下特点 对应类的作用域中最后一个被调用的成员函数。通常用来判断类的作用于结束位置只有一个参数 this 指针且用 ecx 传参。实际上也是 __thiscall 调用约定析构函数没有返回值。或者说返回值为 0如果没有重载析构函数那么一般析构函数都会被优化掉。 因此声明一个局部变量对象的反编译代码如下 int __cdecl main(int argc, const char **argv, const char **envp) {Class a; // [esp4h] [ebp-34h] BYREFClass::Class(a);Class::~Class(a);return 0; }显式调用析构函数不会直接调用类的析构函数而是调用析构代理函数 scalar deleting destructor 。这个函数会根据参数是否为 1 决定是否调用 delete 函数释放 Object 。由于是栈上的对象不需要释放因此传入的参数为 0 。 a.~Class(); // Class::scalar deleting destructor(a, 0); 007C1147 push 0 007C1149 lea ecx,[a] 007C114C call Class::scalar deleting destructor (07C1190h) return 0; 007C1151 mov dword ptr [ebp-44h],0 007C1158 mov dword ptr [ebp-4],0FFFFFFFFh 007C115F lea ecx,[a] 007C1162 call Class::~Class (07C10E0h) 007C1167 mov eax,dword ptr [ebp-44h] 在析构函数外面包裹的一层析构代理函数 scalar deleting destructor 会根据传入的参数是否为 1 决定是否调用 delete 函数释放对象不过真正的析构函数一定会被调用。 ConsoleApplication2.exe!Class::scalar deleting destructor(unsigned int): 008211C0 push ebp 008211C1 mov ebp,esp 008211C3 push ecx 008211C4 mov dword ptr [this],ecx 008211C7 mov ecx,dword ptr [this] 008211CA call Class::~Class (08210E0h) ; 调用 Object 真正的析构函数同样也是 thiscall 008211CF mov eax,dword ptr [ebp8] ; 获取析构函数传入的参数 008211D2 and eax,1 008211D5 je Class::scalar deleting destructor25h (08211E5h) ; 如果传入的参数为 0 则为显式调用析构函数因此直接跳过 delete 。 008211D7 push 30h 008211D9 mov ecx,dword ptr [this] 008211DC push ecx 008211DD call operator delete (082122Ch) ; 调用 delete 函数释放 Object采用 thiscall 调用约定。 008211E2 add esp,8 008211E5 mov eax,dword ptr [this] ; 返回值为 this 指针。 008211E8 mov esp,ebp 008211EA pop ebp 008211EB ret 4 该函数反编译代码如下 Class *__thiscall Class::scalar deleting destructor(Class *this, bool need_free) {Class::~Class(this);if ( need_free )operator delete(this, 0x30u);return this; }堆对象 由于析构函数可以被显式调用因此析构函数还会在栈上传一个参数。如果是显式调用则会传一个 1 否则传一个 0 。 例如这段代码 int main() {Class* anew Class();a-~Class();delete a;return 0; }对应汇编分析如下 a-~Class(); 002F10A6 push 0 ; 参数为 0 表示显式调用析构函数。 002F10A8 mov ecx,dword ptr [a] ; this 指针 002F10AB call Class::scalar deleting destructor (02F10F0h) ; 调用析构代理函数 delete a; 002F10B0 mov edx,dword ptr [a] 002F10B3 mov dword ptr [ebp-1Ch],edx 002F10B6 cmp dword ptr [ebp-1Ch],0 ; 判断 this 指针是否为空如果为空则跳过析构和 delete。 002F10BA je main8Bh (02F10CBh) 002F10BC push 1 ; 参数为 1 表示隐式调用析构函数。 002F10BE mov ecx,dword ptr [ebp-1Ch] ; this 指针 002F10C1 call Class::scalar deleting destructor (02F10F0h) ; 调用析构代理函数 002F10C6 mov dword ptr [ebp-24h],eax ; 析构函数返回值保存在 [ebp-24h] 中 002F10C9 jmp main92h (02F10D2h) ; 直接跳转到函数返回 002F10CB mov dword ptr [ebp-24h],0 ; 因为 this 指针为空因此将析构函数执行结果置为 0 。 return 0; 002F10D2 xor eax,eax 因此反编译代码如下 int __cdecl main(int argc, const char **argv, const char **envp) {Class *a; // [esp14h] [ebp-14h]Class *temp; // [esp18h] [ebp-10h]temp (Class *)operator new(0x30u);if ( temp )a Class::Class(temp);elsea 0;Class::scalar deleting destructor(a, 0);if ( a )Class::scalar deleting destructor(a, 1u);return 0; }如果没有重写类的析构函数那么编译器会优化掉所有显式调用析构函数的代码而隐式调用析构函数会被优化成直接调用 delete 函数释放 Object 。 a-~Class();delete a; 001B1156 mov eax,dword ptr [a] 001B1159 mov dword ptr [ebp-1Ch],eax 001B115C push 30h 001B115E mov ecx,dword ptr [ebp-1Ch] 001B1161 push ecx 001B1162 call operator delete (01B11D5h) 001B1167 add esp,8 001B116A cmp dword ptr [ebp-1Ch],0 001B116E jne main99h (01B1179h) 001B1170 mov dword ptr [ebp-24h],0 001B1177 jmp main0A6h (01B1186h) 001B1179 mov dword ptr [a],8123h 001B1180 mov edx,dword ptr [a] 001B1183 mov dword ptr [ebp-24h],edx 反编译代码如下 int __cdecl main(int argc, const char **argv, const char **envp) {Class *a; Class *temp; temp (Class *)operator new(0x30u);if ( temp )a Class::Class(temp);elsea 0;operator delete(a, 0x30u);return 0; }全局对象静态对象 dynamic_initializer_for__a__ 函数调用 _atexit 注册的 _dynamic_atexit_destructor_for__a__ 函数会直接调用析构函数。 .text:00401DB0 ; void __cdecl dynamic_atexit_destructor_for__a__() .text:00401DB0 _dynamic_atexit_destructor_for__a__ proc near .text:00401DB0 ; DATA XREF: _dynamic_initializer_for__a__D↑o .text:00401DB0 55 push ebp .text:00401DB1 8B EC mov ebp, esp .text:00401DB3 B9 78 33 40 00 mov ecx, offset ?a3VClassA ; this .text:00401DB8 E8 33 F3 FF FF call ??1ClassQAEXZ ; Class::~Class(void) .text:00401DB8 .text:00401DBD 5D pop ebp .text:00401DBE C3 retn反编译代码如下 void __cdecl dynamic_atexit_destructor_for__a__() {Class::~Class(a); }dynamic_initializer_for__a__ 之所以调用 _atexit 不直接注册析构函数是因为析构函数需要传入 this 指针即全局对象地址而 _atexit 注册的函数不能有参数。 对象数组 与构造相似如果一个栈上对象数组作用域结束则程序会依次调用数组中每个对象的析构函数而 Visual C 编译器会将这一过程定义为一个函数 eh vector destructor iterator push offset ??1ClassQAEXZ ; destructor ; eh vector destructor iterator(a, 0x30u, 0xAu, (void (__thiscall *)(void *))Class::~Class); push 0Ah ; count push 30h ; 0 ; size lea ecx, [ebpa] push ecx ; ptr call ??_MYGXPAXIIP6EX0ZZ ; eh vector destructor iterator(void *,uint,uint,void (*)(void *))该函数的参数分别是 数组首地址对象大小对象数量析构函数地址 eh vector destructor iterator 函数会依次为对象数组中的每个对象调用析构函数。异常处理过程就不具体分析了。 void __stdcall eh vector destructor iterator(char *ptr,unsigned int size,unsigned int count,void (__thiscall *destructor)(void *)) {unsigned int v4; // edichar *i; // esiv4 count;for ( i ptr[count * size]; v4--; destructor(i) )i - size; }对于全局对象数组则是在 dynamic_atexit_destructor_for__a__ 函数中调用 eh vector destructor iterator 函数。 void __cdecl dynamic_atexit_destructor_for__a__() {eh vector destructor iterator(a, 0x30u, 0xAu, (void (__thiscall *)(void *))Class::~Class); }而对于堆上对象数组如果指向该数组的指针不为空则会调用 vector deleting destructor 函数。 int __cdecl main(int argc, const char **argv, const char **envp) {Class *v4; // [esp14h] [ebp-14h]_DWORD *block; // [esp18h] [ebp-10h]block operator new[](0x1E4u);if ( block ){*block 10;eh vector constructor iterator(block 1,0x30u,0xAu,(void (__thiscall *)(void *))Class::Class,(void (__thiscall *)(void *))Class::~Class);v4 (Class *)(block 1);}else{v4 0;}if ( v4 )Class::vector deleting destructor(v4, 3u);return 0; }该函数会先调用 eh vector destructor iterator 析构对象数组中的每个成员之后调用 delete 函数释放内存。 Class *__thiscall Class::vector deleting destructor(Class *this, char a2) {if ( (a2 2) ! 0 ){eh vector destructor iterator(this, 0x30u, this[-1].z[9], (void (__thiscall *)(void *))Class::~Class);if ( (a2 1) ! 0 )operator delete[](this[-1].z[9], 48 * this[-1].z[9] 4);return (Class *)((char *)this - 4);}else{Class::~Class(this);if ( (a2 1) ! 0 )operator delete(this, 0x30u);return this;} }对于全局对象数组会在 dynamic_atexit_destructor_for__a__ 函数中调用 eh vector destructor iterator 。 void __cdecl dynamic_atexit_destructor_for__a__() {eh vector destructor iterator(a, 0x30u, 0xAu, (void (__thiscall *)(void *))Class::~Class); }对象的传递 对象作为参数 指针\引用对象传参 无论是指针还是引用传参都是直接把对象地址作为参数传入。 foo(a); 00B71117 lea eax,[a] 00B7111A push eax 00B7111B call foo (0B710C0h) 00B71120 add esp,4 对象传参 浅拷贝 如果类里面没有实现拷贝构造那么直接将对象作为参数传递就是浅拷贝。 浅拷贝和结构体传参类似都是把整个对象复制到参数位置。 foo(a); 00111119 sub esp,30h 0011111C mov ecx,0Ch 00111121 lea esi,[a] 00111124 mov edi,esp 00111126 rep movs dword ptr es:[edi],dword ptr [esi] 00111128 call foo (01110C0h) 0011112D add esp,30h对象传参但时是在调用函数中会将传入的对象进行析构。 void foo(Class a) { 001110C0 push ebp 001110C1 mov ebp,esp printf(%d, a.x); 001110C3 mov eax,dword ptr [a] 001110C6 push eax 001110C7 push 112120h 001110CC call printf (0111040h) 001110D1 add esp,8 } 001110D4 lea ecx,[a] 001110D7 call Class::~Class (01110A0h) 001110DC pop ebp 001110DD ret 深拷贝 如果对象中存在些指针指向申请的内存那么浅拷贝会将这些指针复制一份而在函数内部析构的时候会调用析构函数将这些内存释放。如果调用完函数之后再使用对象内的这些指针指向的内存就会造成 UAF 。 因此这里需要实现拷贝构造函数 Class(const Class a) 。这里参数 a 必须是 const 类型否则无法传参 const 类型的对象。 #include iostreamclass Class { public:int x, y, z[10];char *pMem;Class() {pMem new char[0x100];}Class(const Class a) {pMem new char[0x100];memcpy(pMem, a.pMem, 0x100);x a.x;y a.y;for (int i 0; i 10; i) {z[i] a.z[i];}}~Class() {delete[] pMem;} };void foo(Class a) {printf(%d, a.x); }int main() {const Class a;foo(a);return 0; }此时我们看到再调用 foo 函数前首先会在栈上开辟一块对象所需的内存空间之后会调用拷贝构造函数最后调用 foo 函数。 foo(a); 00D01241 sub esp,34h 00D01244 mov ecx,esp ; 传入拷贝构造函数的 this 指针指向栈上开辟出的一块对象所需的内存空间 00D01246 mov dword ptr [ebp-4Ch],esp 00D01249 lea eax,[a] 00D0124C push eax ; 传入拷贝构造函数的局部变量 a 的地址 00D0124D call Class::Class (0D010F0h) 00D01252 call foo (0D011E0h) 00D01257 add esp,34h 拷贝构造函数本质上就是前面浅拷贝拷贝对象内存的操作交由用户去实现。 void __thiscall Class::Class(Class *this, const Class *a) {int i; // [espCh] [ebp-8h]this-pMem (char *)operator new[](0x100u);qmemcpy(this-pMem, a-pMem, 0x100u);this-x a-x;this-y a-y;for ( i 0; i 10; i )this-z[i] a-z[i]; }由于 __thiscall 是内平栈因此调用完拷贝构造函数之后栈顶就是已经完成初始化的对象参数作为下一步调用的 foo 函数的参数。 同样在 foo 函数中会析构传入的对象。 void __cdecl foo(Class a) {printf(%d, a.x);Class::~Class(a); }对象作为返回值 指针对象 直接返回对象的地址。 int main() { 007E1120 push ebp 007E1121 mov ebp,esp 007E1123 push ecx Class* b foo(); 007E1124 call foo (07E1060h) 007E1129 mov dword ptr [b],eax return 0; 007E112C xor eax,eax } 007E112E mov esp,ebp 007E1130 pop ebp 007E1131 ret 不过如果取返回的对象指针的值赋值给局部变量会形成对象拷贝。 int main() {Class b *foo();return 0; }int __cdecl main() {const Class *v0; // eaxClass b; // [esp50h] [ebp-38h] BYREF__CheckForDebuggerJustMyCode(1C2F97D9_ConsoleApplication2_cpp);v0 foo();Class::Class(b, v0);Class::~Class(b);return 0; }临时对象 例如下面这段代码 Class foo() {Class a;return a; }int main() {foo();return 0; }实际上是向 foo 函数传递一个 main 函数的局部变量地址然后再 foo 函数内部构造在 main 函数析构。这里的 __autoclassinit2 实际上是将对象初始化为全 0 。 在一些版本的编译器中 foo 函数可能会实现为构造一个局部变量 a 然后浅拷贝到函数外部的临时对象但依旧满足函数内构造函数外析构的原则。 Class *__cdecl foo(Class *result) {Class::__autoclassinit2(result, 0x34u);Class::Class(result);return result; }int __cdecl main(int argc, const char **argv, const char **envp) {Class result; // [esp0h] [ebp-34h] BYREFfoo(result);Class::~Class(result);return 0; }如果是使用一个局部变量保存返回的对象 int main() {Class b foo();puts(main end);return 0; }那么该局部变量析构的时间由局部变量作用域决定。 int __cdecl main() {Class b; // [esp50h] [ebp-38h] BYREF__CheckForDebuggerJustMyCode(1C2F97D9_ConsoleApplication2_cpp);foo(b);_puts(main end);Class::~Class(b);return 0; }引用对象 例如下面这段代码 Class foo() {static Class a;return a; }int main() {Class b foo();return 0; }本质还是返回对象的指针。 Class *__cdecl foo() {__CheckForDebuggerJustMyCode(1C2F97D9_ConsoleApplication2_cpp);if ( _TSS0 *(_DWORD *)(*((_DWORD *)NtCurrentTeb()-ThreadLocalStoragePointer _tls_index) 260) ){j___Init_thread_header(_TSS0);if ( _TSS0 -1 ){Class::Class(a);j__atexit(foo_::_2_::_dynamic_atexit_destructor_for__a__);j___Init_thread_footer(_TSS0);}}return a; }Class b foo(); 00C91973 call foo (0C913F2h) 00C91978 mov dword ptr [b],eax 但是如果我们让 b 不在是引用 int main() {Class b foo();return 0; }那么会存在一个拷贝构造 int __cdecl main() {const Class *v0; // eaxClass b; // [esp50h] [ebp-38h] BYREF__CheckForDebuggerJustMyCode(1C2F97D9_ConsoleApplication2_cpp);v0 foo();Class::Class(b, v0);Class::~Class(b);return 0; }无名对象 无名对象就是不用变量存对象而是直接返回 Class foo() {return Class(); }int main() {Class a foo();return 0; }这种本质和返回临时对象一样 Class *__cdecl foo(Class *result) {__CheckForDebuggerJustMyCode(1C2F97D9_ConsoleApplication2_cpp);Class::Class(result);return result; }int __cdecl main() {Class a; // [esp50h] [ebp-38h] BYREF__CheckForDebuggerJustMyCode(1C2F97D9_ConsoleApplication2_cpp);foo(a);Class::~Class(a);return 0; }RTTI运行时类型信息 typeid 是 C 中的运算符用于获取对象的类型信息。 typeid 运算符接受一个表达式作为参数并返回一个表示该表达式类型的 std::type_info 对象。std::type_info 类定义在 typeinfo 头文件中。 std::type_info 类提供了一些成员函数和操作符用于比较类型信息。以下是一些常用的成员函数 name()返回一个指向类型名称的 C 字符串。raw_name():返回一个指向类型名称的内部字符串该字符串可能包含特定于实现的修饰符和命名约定。这个原始名称是特定于编译器和平台的。hash_code()用于获取类型信息的哈希码。before(const std::type_info rhs)比较类型信息之间的顺序。如果当前类型在 rhs 之前则返回 true否则返回 false。before() 函数的比较结果是特定于实现的并且不受 C 标准的具体规定。不同的编译器和平台可能会有不同的比较策略和结果。 为了实现这一功能Visual C 会定义 RTTI 相关结构来存储类相关符号。 这就是为什么 IDA 即使没有符号依旧能识别出虚表的名称 _DWORD *__thiscall sub_412D50(_DWORD *this) {__CheckForDebuggerJustMyCode(unk_41C067);*this CVirtual::vftable;this[1] 1;this[2] 2;puts(CVirtual());return this; }低版本的 Visual C 只有在使用 typeid 相关功能的时候才会出现 RTTI 相关结构而高版本默认开启 RTTI 。可以在 Visual C 的 项目设置 - 配置属性 - C/C - 所有选项 - 启用运行时类型信息 开启或关闭 RTTI 但是即使关闭如果使用 typeid 或者使用 try...catchcatch 有数据类型会强制开启 RTTI 不过不会与虚表关系起来也就是 IDA 不能识别虚表符号。 虚函数 虚函数Virtual Function是面向对象编程中的一个重要概念用于实现多态性Polymorphism。为了实现虚函数Visual C 编译器引入了虚表和虚函数等结构。 注意构造函数不能写为虚函数但是析构函数可以写为虚函数。因为虚函数的调用依赖于对象的类型信息而构造函数在创建对象时用于初始化对象的状态此时对象的类型尚未确定。因此构造函数不能是虚函数。 这里使用如下代码来介绍虚函数 #include iostreamclass CVirtual { public:CVirtual() {m_nMember1 1;m_nMember2 2;puts(CVirtual());}virtual ~CVirtual() {puts(~CVirtual());}virtual void fun1() {puts(fun1());}virtual void fun2() {puts(fun2());}private:int m_nMember1;int m_nMember2; };int main() {CVirtual object;object.fun1();object.fun2();return 0; }虚表 在构造函数中会初始化 CVirtual 中的虚表指针 __vftable 指向 .rdata 段中的虚表 CVirtual::vftable 。 struct __cppobj CVirtual {CVirtual_vtbl *__vftable /*VFT*/;int m_nMember1;int m_nMember2; };void __thiscall CVirtual::CVirtual(CVirtual *this) {this-__vftable (CVirtual_vtbl *)CVirtual::vftable;this-m_nMember1 1;this-m_nMember2 2;_puts(CVirtual()); }其中虚表的类型和虚表的定义如下 struct /*VFT*/ CVirtual_vtbl {void (__thiscall *~CVirtual)(CVirtual *this);void (__thiscall *fun1)(CVirtual *this);void (__thiscall *fun2)(CVirtual *this); };void (__cdecl *const ??_7CVirtual6B[4])() {CVirtual::vector deleting destructor,CVirtual::fun1,CVirtual::fun2,NULL };在析构函数代码如下可以看到再析构函数开始的地方会将类的虚表指针指向该析构函数对应的类的虚表。因为在存在继承的类中子类析构之后在调用子类的虚函数可能会访问到已释放的资源造成 UAF因此需要再析构函数中还原虚表指针。 void __thiscall CVirtual::~CVirtual(CVirtual *this) {this-__vftable (CVirtual_vtbl *)CVirtual::vftable;_puts(~CVirtual()); }因此虚表指针有如下特征 构造函数赋值虚表指针。析构函数还原虚表指针。 即使我们不实现构造析构函数为了安全起见编译器还是会自动生成构造析构函数来赋值和还原虚表指针。不过这种函数比较短通常被优化内联到代码中不以单独函数存在。 另外我们发现虚表中记录的析构函数不是对象真正的析构函数而是析构代理函数 vector deleting destructor 。 CVirtual *__thiscall CVirtual::vector deleting destructor(CVirtual *this, char a2) {CVirtual::~CVirtual(this);if ( (a2 1) ! 0 )operator delete(this, 0xCu);return this; }根据对上述代码的分析可知虚表及在内存中的布局如下 虚表的特征总结如下 不考虑继承的情况下一个类至少有一个虚函数才会存在虚表。不同类的虚表不同相同类的对象共享一个虚表。虚表不可修改通常存放在全局数据区由编译器生成。虚表的结尾不一定为 0 因此从逆向角度不能确定虚表的范围。虚表由函数指针构成。虚表的成员函数顺序按照类中函数声明的顺序排列。对象首地址处保存虚表指针。 虚函数的调用 调用声明虚函数的成员函数实际上是直接 call 的函数地址没有查虚表。 .text:004011B7 lea ecx, [ebpobject] ; this .text:004011BA call ?fun1CVirtualUAEXXZ ; CVirtual::fun1(void)成员函数必须产生多态才会通过虚表调用成员函数。成员函数产生多态的条件有 是虚函数成员函数必须在基类中声明为虚函数使用 virtual 关键字以便在派生类中进行覆盖override。使用指针或者使用引用成员函数必须通过指针或引用进行调用而不是直接通过对象进行调用。这样编译器会在运行时根据实际对象的类型来确定要调用的虚函数。 只要满足这两个条件即便是在析构函数中也可以进行多态强转指针。 另外强转指针是一个很危险的操作以下面这段代码为例虽然强转成 CDerived 但是虚表用的还是 CBase 的虚表因此调用 CDerived 中的函数可能会调用到其它函数或者无效的函数指针。 CBase base;((CDerived *) base)-fun2();例如我们将 main 函数改为下面这种形式 int main() {CVirtual object;CVirtual *p_object object;p_object-fun1();p_object-fun2();return 0; }这时候成员函数是通过虚表调用的。 .text:0040111D 8B 4D E0 mov ecx, [ebpp_object] ; ecx 是 CVirtual 的地址 .text:00401120 8B 11 mov edx, [ecx] ; edx 是虚表地址 .text:00401122 8B 4D E0 mov ecx, [ebpp_object] ; ecx 是 CVirtual 的地址 .text:00401125 8B 42 04 mov eax, [edx4] ; eax 是函数 fun1 的地址 .text:00401128 FF D0 call eax ; 调用 fun1继承 单重继承 这里使用如下代码来介绍单重继承 #include iostreamclass CBase { public:CBase() {m_nMember 1;puts(__FUNCTION__);}virtual ~CBase() {puts(__FUNCTION__);}virtual void fun1() {puts(__FUNCTION__);}virtual void fun3() {puts(__FUNCTION__);}private:int m_nMember; };class CDerived : public CBase { public:CDerived() {m_nMember 2;puts(__FUNCTION__);}~CDerived() {puts(__FUNCTION__);}virtual void fun1() {puts(__FUNCTION__);}virtual void fun2() {puts(__FUNCTION__);}private:int m_nMember;CBase base; };int main() {CDerived Derived;return 0; }构造析构顺序 在类的构造和析构过程中并不仅仅执行用户定义的构造和析构函数还涉及到其他构造和析构操作的顺序。 构造顺序 构造基类构造成员对象对象内部定义的一些成员变量构造自身 析构顺序 析构自身析构成员对象析构基类 这里有以下几点需要注意 构造析构顺序通常是我们还原类的继承关系的一个重要依据不过这里要区分基类和成员对象。 区分基类和成员对象的构造可以根据传入的 this 指针。基类传的是整个对象的地址而成员对象传的是成员变量的地址。如果这两个地址相同就根据代码可读性还原。 基类的构造一定在修改虚表指针之前而成员对象的构造时间看编译器版本。 对于老版本编译器例如 VC 6.0成员对象的构造在修改虚表之前。对于新版本编译器成员对象的构造在修改虚表之后。也可以作为区分基类和成员对象的一个依据 构造函数 void __thiscall CBase::CBase(CBase *this) {this-__vftable (CBase_vtbl *)CBase::vftable;this-m_nMember 1;_puts(CBase::CBase); }void __thiscall CDerived::CDerived(CDerived *this) {CBase::CBase(this);this-__vftable (CDerived_vtbl *)CDerived::vftable;CBase::CBase(this-base);this-m_nMember 2;_puts(CDerived::CDerived); }析构函数 void __thiscall CBase::~CBase(CBase *this) {this-__vftable (CBase_vtbl *)CBase::vftable;_puts(CBase::~CBase); }void __thiscall CDerived::~CDerived(CDerived *this) {this-__vftable (CDerived_vtbl *)CDerived::vftable;_puts(CDerived::~CDerived);CBase::~CBase(this-base);CBase::~CBase(this); }内存结构 派生类的虚表填充过程 复制基类的虚表函数顺序不变。如果派生类虚函数中有覆盖基类的虚函数与基类的对应函数同名同参使用派生类的虚函数地址覆盖对应表项。如果派生类有新增的虚函数将其放在虚表后面。 派生类的对象填充过程 虚表指针指向派生类对应的虚表。将派生类新增的成员放到基类的成员后面。 因此示例代码中的 CBase 和 CDerived 类的实例化的对象和虚表结构如下 虚表函数重合是还原类继承关系的一个重要依据。 多重继承 这里使用如下代码来介绍多重继承 #include iostreamclass CBase1 { public:CBase1() {m_nMember 1;puts(__FUNCTION__);}virtual ~CBase1() {puts(__FUNCTION__);}virtual void fun1() {puts(__FUNCTION__);}virtual void fun2() {puts(__FUNCTION__);}virtual void fun3() {puts(__FUNCTION__);}private:int m_nMember; };class CBase2 { public:CBase2() {m_nMember 2;puts(__FUNCTION__);}virtual ~CBase2() {puts(__FUNCTION__);}virtual void fun1() {puts(__FUNCTION__);}virtual void fun4() {puts(__FUNCTION__);}virtual void fun5() {puts(__FUNCTION__);}private:int m_nMember; };class CDerived : public CBase1,public CBase2 { public:CDerived() {m_nMember 3;puts(__FUNCTION__);}~CDerived() {puts(__FUNCTION__);}virtual void fun2() {puts(__FUNCTION__);}virtual void fun4() {puts(__FUNCTION__);}virtual void fun6() {puts(__FUNCTION__);}private:int m_nMember;CBase1 base1;CBase2 base2; };int main() {CDerived Derived;return 0; }内存结构 构造析构顺序 虚表
http://www.w-s-a.com/news/527646/

相关文章:

  • 洛阳霞光建设网站html做分模块的网站
  • 域名建议网站wordpress 伪静态html
  • 网站风格化设计方案免费模式营销案例
  • 凤翔网站建设农村建设自己的网站首页
  • 怎样用网站做单笔外贸建筑设计公司合作加盟
  • 建网站买的是什么网站开发三层结构
  • wordpress图纸管理网站2345网址导航智能主版
  • 想调用等三方网站数据该怎么做培训课程
  • 高端营销网站建设wordpress咨询
  • 网站搜索框如何做创业怎么做网站
  • 网站手机版管理链接产品推广找哪家公司
  • vuejs 可做网站吗蜘蛛互联网站建设
  • 沈阳网站备案查询17zwd一起做业网站
  • 石家庄大型公司建站广州设计网站培训学校
  • 如何让百度收录中文域名网站wordpress前台管理评论
  • 铁岭 建筑公司网站 中企动力建设佛山app开发公司
  • 网站开发用的电脑深圳专业网站建设服务
  • 内容营销价值wordpress博客优化插件
  • 最优惠的郑州网站建设淘宝网商城
  • 做封面网站企业网站优化服务商
  • 电子商务网站设计是什么蚌埠铁路建设监理公司网站
  • .name后缀的网站做房产网站多少钱
  • 手机上传网站源码网站app封装怎么做
  • 做的网站放在阿里云网站建设投标书范本
  • 做文化传播公司网站wordpress仿简书
  • 什么网站有题目做西宁网站制作哪里好
  • 网站上添加图片的原则优易主机 wordpress
  • 用php做的网站源代码那里有做像美团的网站的
  • 网站建设百科有什么做兼职的网站
  • 创造网站电商网站建设方案道客巴巴