zoukankan      html  css  js  c++  java
  • 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 
    ":"=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 
    ":"=r"(ebp_reg));

    编译通过。 
    下面一堆警告,还是64位指针惹的祸。 
    将所有源文件中的int换成了long,main函数的int返回类型可以保留,再编译,警告消失。
    但是运行./test 
    意料之外,无输出。

    把test.c换成了一个更简单的文件来debug

    #include "minicrt.h"
    int main()
    {
            printf("hello world
    ");
            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
    ",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,是不是看到输出了?

    来源:http://www.tuicool.com/articles/Zzqye2

  • 相关阅读:
    c++类中的常量
    什么是租赁
    期末结转销售成本结算利润
    合并报表抵消分录借:营业成本 贷:存货怎么理解(自己分析)
    电脑上装安卓系统
    下载apps from google play on windows 10
    virtualbox找不到ubuntu64选项
    如何区分其他综合收益和当期损益
    哪些项目进其他综合收益
    境外经营财务报表的折算方法
  • 原文地址:https://www.cnblogs.com/tibetanmastiff/p/4156228.html
Copyright © 2011-2022 走看看