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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM