Docker设置
首先选择ubuntu 20.04镜像,按照docker官网指引,安装docker。
之后编写 Dockerfile
,如下
FROM scratch ADD ubuntu-bionic-oci-i386-root.tar.gz / WORKDIR / COPY rootfs / RUN apt update RUN apt install -y openssh-server psmisc net-tools
由于host系统已经是64位ubuntu了,所以直接 FROM scratch
,之后将从获取得到的32位ubuntu rootfs拷贝到容器中的根目录。之后将项目目录中 rootfs
拷贝到容器的根目录,覆盖sources.list、覆盖ssh-key、root账号的authorized_key。
之后编写 docker-compose.yml
如下,将ssh的22端口映射到host机器的5022,设置启动运行脚本,防止栈地址随机化。
version"3"services i386 container_name i386 image i386 build context DockerBuild/i386 dockerfile Dockerfile command /bin/bash /docker-entrypoint.d/run.sh ports"5022:22" volumes ./DockerBuild/i386:/docker-entrypoint.d # GDB security_opt seccomp:unconfined cap_add SYS_PTRACE
测试代码
主要测试C++中虚函数表的实现方法,基本的测试代码如下:
classPlainPerson{ public: constchar*get_job() { return"none"; } }; classPerson { public: virtualvoidplaceholder() {}; virtualconstchar*get_job() { return"none"; } }; classStudent: publicPerson { public: virtualconstchar*get_job() { return"student"; } }; voidprint_job(Person*p){ printf("job for 0x%X is '%s'\n", p, p->get_job()); } intmain() { printf("sizeof(void) = %d\n", sizeof(void)); printf("sizeof(PlainPersono) = %d\n", sizeof(PlainPerson)); printf("sizeof(Persono) = %d\n", sizeof(Person)); Person*p0=newPerson(); Person*p1=newStudent(); print_job(p0); print_job(p1); return0; }
实验结果
使用 g++ -fno-pic -no-pie main.cpp
编译代码,使用 objdump -Mintel -d a.out
查看反汇编。
08048546 <_Z9print_jobP6Person>: 8048546: 55 push ebp 8048547: 89 e5 mov ebp,esp 8048549: 83 ec 08 sub esp,0x8 804854c: 8b 45 08 mov eax,DWORD PTR [ebp+0x8] 804854f: 8b 00 mov eax,DWORD PTR [eax] 8048551: 83 c0 04 add eax,0x4 8048554: 8b 00 mov eax,DWORD PTR [eax] 8048556: 83 ec 0c sub esp,0xc 8048559: ff 75 08 push DWORD PTR [ebp+0x8] 804855c: ff d0 call eax 804855e: 83 c4 10 add esp,0x10 8048561: 83 ec 04 sub esp,0x4 8048564: 50 push eax 8048565: ff 75 08 push DWORD PTR [ebp+0x8] 8048568: 68 0d 87 04 08 push 0x804870d 804856d: e8 9e fe ff ff call 8048410 <printf@plt> 8048572: 83 c4 10 add esp,0x10 8048575: 90 nop 8048576: c9 leave 8048577: c3 ret
函数里面有两个 call
,应该分别对应 get_job
和 printf
。首先第一个 call
的地址来自寄存器 eax
,除去调整栈大小和传参操作,在 call
之前只剩下4如下指令:
mov eax,DWORD PTR [ebp+0x8] // 获取第一个参数,即 Person* p; mov eax,DWORD PTR [eax] // f* f_arr = *(f**)(p); add eax,0x4 mov eax,DWORD PTR [eax] // f func = f_arr[1];
说明编译器将实现多态的虚函数表指针(即 f**),放到了对象的头4字节中。
由于 virtual void placeholder()
的影响,获取 get_name
的实际位置需要访问虚函数表中的第二项(第一项留给了来自父类的placeholder函数)。
使用gdb验证我们的猜想,如下所示:
(gdb) b *0x804855c Breakpoint 1 at 0x804855c (gdb) c The program is not being run. (gdb) run Starting program: /root/test/run sizeof(void) = 1 sizeof(PlainPersono) = 1 sizeof(Persono) = 4 Breakpoint 1, 0x0804855c in print_job(Person*) () (gdb) i r eax eax 0x804863a 134514234 (gdb) x $eax 0x804863a <_ZN6Person7get_jobEv>: 0xb8e58955
根据反编译结果打断点,之后使用 x
命令查看 eax
寄存器对应内存区域的内容,显示是一个函数。查看反汇编可以确认此函数就是实际调用的虚函数。
总结
GCC将虚函数表的指针放到对象的前4个字节,在调用时候,从虚函数表中获取真实的函数指针。这种设计只要保证父类和子类的虚函数表对应位置保持相同语义即可。