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编译代码

# gcc -c -fno-builtin -nostdlib entry.c malloc.c stdio.c string.c printf.c
# ar -rs minicrt.a malloc.o printf.o stdio.o string.o
# gcc -c -ggdb -fno-builtin -nostdlib test.c
# ld -static -e mini_crt_entry entry.o test.o minicrt.a -o test

但是在第一句的时候,entry.c就无法通过编译。错误信息:

entry.c:59: Error: unsupported instruction `mov'

打开发现错在一句内联汇编上:

//ebp_reg = %ebp
asm("movl %%ebp,%0 \n":"=r"(ebp_reg));

我学汇编写汇编都是在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

#include "minicrt.h"
int main()
{
        printf("hello world\n");
        return 0;
}

单步跟踪发现,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调用之前

18 printf("%d %s\n",len,buf);
00000000004014db: mov -0x10(%rbp),%rdx
00000000004014df: mov -0x18(%rbp),%rax
00000000004014e3: mov %rax,%rsi
00000000004014e6: mov $0x4015f6,%edi
00000000004014eb: mov $0x0,%eax
00000000004014f0: callq 0x400e5b

进入printf

printf:
0000000000400e5b: push %rbp
0000000000400e5c: mov %rsp,%rbp
0000000000400e5f: sub $0xd0,%rsp
0000000000400e66: mov %rsi,-0xa8(%rbp)
0000000000400e6d: mov %rdx,-0xa0(%rbp)
0000000000400e74: mov %rcx,-0x98(%rbp)
0000000000400e7b: mov %r8,-0x90(%rbp)
0000000000400e82: mov %r9,-0x88(%rbp)
0000000000400e89: test %al,%al
0000000000400e8b: je 0x400ead
0000000000400e8d: movaps %xmm0,-0x80(%rbp)
0000000000400e91: movaps %xmm1,-0x70(%rbp)
0000000000400e95: movaps %xmm2,-0x60(%rbp)
0000000000400e99: movaps %xmm3,-0x50(%rbp)
0000000000400e9d: movaps %xmm4,-0x40(%rbp)
0000000000400ea1: movaps %xmm5,-0x30(%rbp)
0000000000400ea5: movaps %xmm6,-0x20(%rbp)
0000000000400ea9: movaps %xmm7,-0x10(%rbp)
0000000000400ead: mov %rdi,-0xc8(%rbp)

之前写操作系统,也自己实现过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由

int printf(const char *format,...)
{
        va_list(arglist);
        va_start(arglist,format);
        return vfprintf(stdout,format,arglist);
}

修改为

long printf(const char *format,...)
{
        char* arglist;
        asm( "movq %%rbp,%0":"=r"(arglist) );
        arglist -= 0xa8;
        return vfprintf(stdout,format,arglist);
}

好了,再输入readme.txt里的四条命令,运行test,是不是看到输出了?

posted @ 2013-08-04 15:38  南树  阅读(1884)  评论(0编辑  收藏  举报