一個最簡單的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.S 或 sysdeps/x86_64/start.S,
_start 調用了__libc_start_main 等函數,__libc_start_main在csu/libc-start.c ,可以看到:
result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
這就是main被調用的地方。這個是我們用戶代碼開始的地方
所以,在_start 和 main 之間的那些函數都是做一些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開始,調用main,main函數從上到下執行,從地址80480dd執行到80480f0,call返回后,下一條指令又是80480dd,又會調用main,從地址80480dd執行到80480f0 又執行一遍,第二次執行,其實不是本意,
只是剛好call后面的指令恰好是main函數,一直在執行到ret指令,這時,棧頂的值是0x1, 把該值給eip,0x1地址上無有效指令,報段異常,正確的段應該在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