zoukankan      html  css  js  c++  java
  • Linux下一个最简单的不依赖第三库的的C程序(2)


    一个最简单的C程序,如下:

    main.c:

    int main()
    {
       char *str = "Hello World";
       return 8;
    }

    在64位平台上编译一个32位的程序,如下:(32位只是为了演示方便)

    gcc -m32 -o hello main.c
    ./hello
    echo $?

    运行后 会看到结果是 8,说明程序正常。$?表示查看上一个命令的返回值


    统计一下程序大小:7263字节,

    wc -c hello
    7263 hello

    通过 ldd 查看动态库的依赖,发现如下的一些动态库依赖,libc.so,可以说是几乎所有Linux上的程序都会依赖的一个基础C函数库。 ld-linux.so 是一个动态dll的加载库,它会动态加载libc.so, dll在Linux shared object, 后缀是 .so linux-gate.so是一个伪文件,它是与内核联系的一个接口,64位程序的linux-vdso.so也是同样的作用

    ldd hello
    linux-gate.so.1 => (0xf7780000)
    libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf75ae000)
    /lib/ld-linux.so.2 (0x5662a000)


    我们加上 -nostdlib 选项,该选项在linking链接阶段,不会把标准库和启动文件引入进来,下面提示没有找到_start这个函数符号,默认给了一个入口地址。
    我们不管这个警告,执行程序,发现报段错误,如果统计这个可执行文件的大小,会发现大小有1128字节,比之前的7236小了好多。

    gcc -m32 -nostdlib -o hello main.c
      /usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 00000000080480d8
    ./hello
      Segmentation fault
    ldd hello
      not a dynamic executable    
    wc -c hello
      1128 hello

    当使用 -nostdlib 编译的时候,提示没有找到_start函数符号,那我们会不会想到是glibc里面提供的某个函数呢?
    Linux上的C程序,入口实际是_start,而不是我们代码里的main, _start定义在ctr1.o这个可重定位目标文件里

    查找一下 ctr1.o 这个文件, 找到文件位置。
    sudo find / -name "crt1.o"

    只编译,不链接,-c表示只编译,生成一个32位的hello.o的目标文件

    gcc -m32 -Os -c main.c -o hello.o

    进行链接:

    ld /usr/lib/i386-linux-gnu/crt1.o -o hello hello.o
    #64位crt1.o 在/usr/lib/x86_64-linux-gnu/crt1.o

    发现报错:

    /usr/lib/i386-linux-gnu/crt1.o: In function `_start':
    (.text+0xc): undefined reference to `__libc_csu_fini'
    /usr/lib/i386-linux-gnu/crt1.o: In function `_start':
    (.text+0x11): undefined reference to `__libc_csu_init'
    /usr/lib/i386-linux-gnu/crt1.o: In function `_start':
    (.text+0x1d): undefined reference to `__libc_start_main'

    crt1.o对应的代码在glibc的源代码,在glibc-2.19 源代码目录下的 sysdeps/i386/start.Ssysdeps/x86_64/start.S
    _start 调用了__libc_start_main 等函数,__libc_start_maincsu/libc-start.c ,可以看到:

    result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);

    这就是main被调用的地方。这个是我们用户代码开始的地方

    所以,在_startmain 之间的那些函数都是做一些glibc的准备工作,我们这里要剥离glibc的依赖,所以,我们直接可以这样做:
    增加一个文件:
    stubstart.s

    .globle _start
    _start:call main    

    编译:

    gcc -m32 -nostdlib -Os stubstart.s main.c -o hello
    ./hello #运行报错:Segmentation fault    

    让我使用加上调试信息,重新编译,用gdb来看看

    gcc -m32 -g -nostdlib -O0 stubstart.s main.c -o hello

    首先反汇编看看,不同的编译优化,汇编代码会有一些不同:

    objdump -d hello
    
    hello: file format elf32-i386
    Disassembly of section .text: 
    080480d8 <_start>:
    80480d8:    e8 00 00 00 00       call 80480dd <main>          #等价于:push %eip   ;   mov 80480dd, %eip
    
    080480dd <main>:
    80480dd:    55                   push %ebp
    80480de:    89 e5                mov %esp,%ebp                #这2句是所有函数调用都有的,序言,套路,重新设置栈顶
    80480e0:    83 ec 10             sub $0x10,%esp               #分配临时变量
    80480e3:    c7 45 fc f1 80 04 08 movl $0x80480f1,-0x4(%ebp)   #字符串赋值
    80480ea:    b8 08 00 00 00       mov $0x8,%eax                #这里是把8赋值给eax,如果是把0赋值给eax,指令可能是:xor %eax,%eax 。 
    # xor 异或自己就是0,与 movl $0, %eax 具有类似效果 80480ef: c9 leave #等价于:mov %ebp,%esp ; pop %esp ;
    # 同时esp自动加一个步进(往栈基移动一个步进),
    # 即80480dd地址后的2个指令反向执行,回归原位 80480f0: c3 ret #等价于 pop
    %eip,是call指令的反向执行,回归原位

    我们用gdb来看看

    gdb ./main
    gdb> break _start
    gdb> run    

    到_start 函数命中断点,开始 stepi 单步的执行汇编指令。

    程序从_start开始,调用mainmain函数从上到下执行,从地址80480dd执行到80480f0call返回后,下一条指令又是80480dd,又会调用main,从地址80480dd执行到80480f0 又执行一遍,第二次执行,其实不是本意,
    只是刚好call后面的指令恰好是main函数,一直在执行到ret指令,这时,栈顶的值是0x1, 把该值给eip0x1地址上无有效指令,报段异常,正确的段应该在80480f0附近

    这里主要就是因为 call返回后,下一条指令又是80480dd,恰好是main函数,gcc编译的时候,如果使用 -Os 选项,得到的反汇编如下,这时就不会执行2次,但是依然会报段错误,因为指令指针已经飞到其他地方去了。

    080480d8 <main>:
    80480d8:    55             push %ebp
    80480d9:    b8 08 00 00 00 mov $0x8,%eax
    80480de:    89 e5          mov %esp,%ebp
    80480e0:    5d             pop %ebp
    80480e1:    c3             ret 
    080480e2 <_start>:
    80480e2:    e8 f1 ff ff ff call 80480d8 <main>

    正确的做法就是:在call main之后,退出进程。

    .globl _start
    _start:  call main
             mov $01, %eax
             mov $00, %ebx 
             int $0x80

    调用系统调用_exit(0);正确退出。

    反汇编如下:

    080480d8 <_start>:
    80480d8:    e8 0c 00 00 00      call 80480e9 <main>
    80480dd:    b8 01 00 00 00      mov $0x1,%eax
    80480e2:    bb 00 00 00 00      mov $0x0,%ebx
    80480e7:    cd 80               int $0x80    #END
    
    080480e9 <main>:
    80480e9:    55                   push %ebp
    80480ea:    89 e5                mov %esp,%ebp
    80480ec:    83 ec 10             sub $0x10,%esp
    80480ef:    c7 45 fc fd 80 04 08 movl $0x80480fd,-0x4(%ebp)
    80480f6:    b8 08 00 00 00       mov $0x8,%eax
    80480fb:    c9                   leave 
    80480fc:    c3                   ret    

    如果用C语言来代替之前那个_start的汇编,则如下:
    stubstart.c

    void _start() {
    
     // main body of program: call main(), etc
    
         // exit system call 
          asm("mov $1,%eax;"
              "xor %ebx,%ebx;"
              "int $0x80"
              // xor 异或自己就是0,与 movl $0, %eax 具有类似效果
          );
    }    

    至此,一个完全不依赖第三方库的简单C函数就完成了。

    参考:

    https://www.cnblogs.com/TOLLA/p/9646035.html
    https://blogs.oracle.com/linux/hello-from-a-libc-free-world-part-1-v2
    https://blogs.oracle.com/linux/hello-from-a-libc-free-world-part-2-v2

  • 相关阅读:
    学生排序,使用三层优先级
    利用类计算学生成绩和排序
    join()函数
    对象
    015_eclipse开发环境的使用
    013_运算符_算术
    012_变量
    011_jdk7新特性
    010_类型提升问题
    008_浮点数误差问题
  • 原文地址:https://www.cnblogs.com/softfair/p/hello-from-a-glibc-free-world.html
Copyright © 2011-2022 走看看