MiniCRT 64位 linux 系统移植记录:64位gcc的几点注意
32位未修改源码与修改版的代码下载:
git clone git@github.com:youzhonghui/MiniCRT.git
MiniCRT 64位 linux 系统移植记录
MiniCRT是《程序员的自我修养:链接,转载于库》的作者俞甲子写的小型的C运行时库。里面提供了printf,malloc,free,fopen等比较常用的函数实现。
之所以要捣鼓这个东西,是因为要自己写一个链接器,链接标准库的时候出了麻烦,一些符号在整个libc中都找不到定义,标准库又太大,研究源码,翻文档都不方便,不如拿一个小巧可用的MiniCRT过来,源码在手,知根知底。
但是也不是一帆风顺,我现在用的系统是64位的archlinux,俞甲子在写书的时候用的还是32位系统。搬运到64位系统上还遇上写麻烦,但是比较64位是趋势了,不能老窝在32位里,在前人经验的庇护下学习吧。所以捣騰了一天,修改了源码,把他移植到64位的linux系统上来,这个过程也学到一些有趣的东西。下面是过程记录。
下了源码,按照readme.txt编译代码
但是在第一句的时候,entry.c就无法通过编译。错误信息:
打开发现错在一句内联汇编上:
我学汇编写汇编都是在windows下,对AT&T的汇编语法不熟,谷歌之,找到一篇好资料:
http://argcandargv.com/articles/84.c
语法上这句汇编没错,我也是在几次试验以后猛然发现指针竟然是64位的。我这才意识到我真的是在64位系统上阿(你特么不是一直在用吗 – -)。那么错误很明显了,movl 和 ebp是32位的,%0即ebp_reg是64位的。
修改为
asm("movq %%rbp,%0 \n":"=r"(ebp_reg));
编译通过。
下面一堆警告,还是64位指针惹的祸。
将所有源文件中的int换成了long,main函数的int返回类型可以保留,再编译,警告消失。
但是运行./test
意料之外,无输出。
把test.c换成了一个更简单的文件来debug
单步跟踪发现,int 0×80的4号中断不好使了。网上也没找到相关的信息。
我和小伙伴们都有点心灰意冷(要是64位系统不支持这个4号中断,我还搞个蛋啊!)
但是在一股不甘心的力量驱动下,又做了几次试验,把这段代码独立出来,编成32位,运行,惊奇发现,输出hello world了。
那么64位系统还是支持这个系统调用的,为什么32位可以,而64位不行?
猜测:
这个中断只能输出4GB以内地址的字符串,也就是支持ecx,但是不支持rcx。
验证的试验很容易做,发现确是是这样。
readelf -s test
一看,全局变量,静态变亮的地址都在 0×400000 – 0x60FFFFF 之内。那么能越界的就是栈中的局部变量了。
那么我必须要在调用4号中断之前,把栈里的内容拷贝到全局变量中,然后把全局变量指针交给4号中断,这样就解决越界的问题了。
修改了fputc和fputs函数:
static char __fputc_tmp_val__ = 0; long fputc(char c,FILE* stream) { __fputc_tmp_val__ = c; if (fwrite(&__fputc_tmp_val__,1,1,stream) != 1) { return EOF; } else { return c; } } static char __fputs_tmp_array__[256] = {0}; static int __fputs_tmp_size__ = 256; long fputs(const char* str,FILE *stream) { long len = strlen(str); if( len >= __fputs_tmp_size__ ) return EOF; strcpy( __fputs_tmp_array__,str ); if (fwrite(__fputs_tmp_array__,1,len,stream) != len) { return EOF; } else { return len; } }
测试,顺利输出hello world
原以为这样就大功告成了,但是换回原来的tes进入t.c一试,又没有输出。
晕,单步!
发现参数根本没有正确传递。看反汇编:
printf调用之前
进入printf
之前写操作系统,也自己实现过printf,但是..但是,这是妹啊!为什么参数没有通过栈传递!
找资料,同时心中默默将gcc骂了十遍。
找到一篇资料:http://blog.csdn.net/videosender/article/details/6425671
我从里面摘出比较重要的一段:
「而GCC的调用约定跟VC不同。前6个整数参数会依次放到rdi, rsi, rdx, rcx, r8, r9中,前8个浮点参数放到xmm0到xmm7中。除了使用了更多的寄存器,与vc不同的是,整数和浮点数寄存器是混合使用的不用为没用的参数预留。还是刚才的例子,第一个参数是int,第二个是double,第三个char*,第四个double,参数数会依次放到 rdi,xmm0,rsi,xmm1. 另外,没有在栈上预留寄存器区。 更多的参数和vc一样,放在栈上。」
通过试验发现,通过寄存器传递参数这个设置没办法通过__attribute__((regparm(0)))来关闭。
这样只能修改代码了。
可以看到,要实现一个寄存器参数版的va_start,va_arg,va_end比较麻烦,我又不想修改过多代码。
观察发现,在-O0优化选项下(gcc的默认选项),进入printf后,会先把rsi,rdx…这些寄存器挨个放入栈。如上面所示,不过实际传入的参数个数有多少。
但是比较奇怪的是,应该是rdi为第一个参数,但是rdi并没有出现在rsi之前。
别忘了,printf的第一个参数是显示声明的,是一个字符串,上边汇编的最后一句,mov %rdi,-0xc8(%rbp)就表明正是如此。
那么我们要的参数列表就从rsi开始,它被复制到-0xa8(%rbp)的位置。Check!这就是我们要找的位置。
另外有一点很需要注意的是,浮点参数会放到xmm0到xmm7中,从上面的汇编可以看出,rsi,rdx..xmm0…的排列顺序是固定的。在复制xmm0-xmm7之前,有一句test %al ,%al,当调用printf时,有传入浮点参数时eax=1,否则为0。超过六个的整数参数会被压入栈中。
好了,只要不传入浮点参数,那么我们就可以通过0xa8的偏移来找到arg_list。而MiniCRT的printf也没有支持浮点输出,那么,我们就取巧吧。
将printf由
修改为
好了,再输入readme.txt里的四条命令,运行test,是不是看到输出了?