一)汇编源程序分析:
编写一个AT&T的汇编语言程序,使程序在运行时睡眠10秒钟
源代码如下:
#include "sys/syscall.h"
.data
sleeptime:
.long 10,0
.text
.global _start
.type _start, @function
_start:
movl $SYS_nanosleep, %eax
movl $sleeptime, %ebx
int $0x80
movl $SYS_exit, %eax
movl $0, %ebx
int $0x80
1)汇编语言加载了C的头文件
##############################################################
#include "sys/syscall.h"
##############################################################
加载了sys/syscall.h头文件,syscall.h中定义了系统调用的宏.
而POSIX函数是系统调用的简单封装,这里指的POSIX函数就是类似于open,close,read等
也就是说open不是系统调用,而是POSIX函数,只是这个POSIX函数包含了由内核赋值的系统调用.
例如:
#include <syscall.h>
n = syscall(SYS_read, fd, buffer,length);
在上面的例子中,系统调用由宏SYS_read定义,而read函数则对上面的系统调用进行封装.
2)Linux使用的Syscall技术
Linux使用的syscall技术称为应用程序的二进制接口(ABI),它与应用程序接口(API)不同,API要求链接兼容的函数,
而ABI不要求链接不需要运行的代码
也就是说,当你直接调用ABI(SYSCALL)的时候,是不需要链接库,而调用API,例如read,则需要链接库,例如libc等等.
3)代码段与数据段
###############################################################
.data
sleeptime:
.long 10,0
###############################################################
Linux 是一个运行在保护模式下的 32 位操作系统,采用 flat memory 模式,目前最常用到的是 ELF 格式的二进制代码.
一个 ELF 格式的可执行程序通常划分为如下几个部分:.text、.data 和 .bss,其中 .text 是只读的代码区,.data 是可读可写的数据区,
而 .bss 则是可读可写且没有初始化的数据区。
代码区和数据区在 ELF 中统称为 section,根据实际需要你可以使用其它标准的 section,也可以添加自定义 section,但一个 ELF 可执行程序至少应该有一个 .text 部分
这里用了数据段.data,并定义了sleeptime: .long 10.0,long 即32位bit的长度
4)代码段的入口
###############################################################
.text
.global _start
.type _start, @function
###############################################################
这里定义了代码段.text,定义了全局入口为_start,ld链接器会默认选择_start做为符号入口,如果在程序中不指定_start做为全局入口,编译将得到下面的警告,但不影响运行
/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000008048074
注意指定_start做为全局程序入口,而在链接时同时也链接了/usr/lib/crt1.o,将报错,因为crt1.o已经提供了一个_start入口点,这里有就会出现多重定义,所以报错.
一个C程序的入口点是main函数,其实是不准确的,真正的入口点是_start,因为ld会默认把crt.o和目标程序链接在一起,也就是程序先调用_start(startup routine),再通过_start调用main函数.
5)系统调用
###############################################################
_start:
movl $SYS_nanosleep, %eax
movl $sleeptime, %ebx
int $0x80
movl $SYS_exit, %eax
movl $0, %ebx
int $0x80
###############################################################
这里调用了SYS_nanosleep和SYS_exit的系统调用.类似于C的read函数的封装调用:
n = syscall(SYS_read, fd, buffer,length);
二)编译与链接
以上的程序通过下面的语句进行编译:
gcc -o basic -nostdlib basic.S
1)这里为什么要指定-nostdlib呢.
答案是因为我们不需要main函数的调用,GCC编译器默认会在main函数的开始部分产生一个__main函数的调用,如果我们使用-nostdlib编译程序,将不会调用__main函数,自然也不会在链接的时候加上-lgcc.
在编译时加上-v,来打印出编译的过程:
gcc -o basic -nostdlib basic.S -v
Using built-in specs.
Target: i486-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Debian 4.3.2-1.1' --with-bugurl=file:///usr/share/doc/gcc-4.3/README.Bugs --enable-languages=c,c++,fortran,objc,obj-c++ --prefix=/usr --enable-shared --with-system-zlib --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --enable-nls --with-gxx-include-dir=/usr/include/c++/4.3 --program-suffix=-4.3 --enable-clocale=gnu --enable-libstdcxx-debug --enable-objc-gc --enable-mpfr --enable-targets=all --enable-cld --enable-checking=release --build=i486-linux-gnu --host=i486-linux-gnu --target=i486-linux-gnu
Thread model: posix
gcc version 4.3.2 (Debian 4.3.2-1.1)
COLLECT_GCC_OPTIONS='-o' 'basic' '-nostdlib' '-v' '-mtune=generic'
/usr/lib/gcc/i486-linux-gnu/4.3.2/cc1 -E -lang-asm -quiet -v basic.S -mtune=generic -fno-directives-only -o /tmp/ccsjdQ1s.s
ignoring nonexistent directory "/usr/local/include/i486-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../i486-linux-gnu/include"
ignoring nonexistent directory "/usr/include/i486-linux-gnu"
#include "..." search starts here:
#include <...> search starts here:
/usr/local/include
/usr/lib/gcc/i486-linux-gnu/4.3.2/include
/usr/lib/gcc/i486-linux-gnu/4.3.2/include-fixed
/usr/include
End of search list.
COLLECT_GCC_OPTIONS='-o' 'basic' '-nostdlib' '-v' '-mtune=generic'
as -V -Qy -o /tmp/cc6h5zXO.o /tmp/ccsjdQ1s.s
GNU assembler version 2.18.0 (i486-linux-gnu) using BFD version (GNU Binutils for Debian) 2.18.0.20080103
COMPILER_PATH=/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/:/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/:/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/
LIBRARY_PATH=/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib/:/lib/../lib/:/usr/lib/../lib/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-o' 'basic' '-nostdlib' '-v' '-mtune=generic'
/usr/lib/gcc/i486-linux-gnu/4.3.2/collect2 --eh-frame-hdr -m elf_i386 --hash-style=both -dynamic-linker /lib/ld-linux.so.2 -o basic -L/usr/lib/gcc/i486-linux-gnu/4.3.2 -L/usr/lib/gcc/i486-linux-gnu/4.3.2 -L/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib -L/lib/../lib -L/usr/lib/../lib -L/usr/lib/gcc/i486-linux-gnu/4.3.2/../../.. /tmp/cc6h5zXO.o
2)加-nostdlib编译与链接的过程:
第一步:
/usr/lib/gcc/i486-linux-gnu/4.3.2/cc1 -E -lang-asm -quiet -v basic.S -mtune=generic -fno-directives-only -o /tmp/ccsjdQ1s.s
第二步:
as -V -Qy -o /tmp/cc6h5zXO.o /tmp/ccsjdQ1s.s
第三步:
/usr/lib/gcc/i486-linux-gnu/4.3.2/collect2 --eh-frame-hdr -m elf_i386 --hash-style=both -dynamic-linker /lib/ld-linux.so.2 -o basic -L/usr/lib/gcc/i486-linux-gnu/4.3.2 -L/usr/lib/gcc/i486-linux-gnu/4.3.2 -L/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib -L/lib/../lib -L/usr/lib/../lib -L/usr/lib/gcc/i486-linux-gnu/4.3.2/../../.. /tmp/cc6h5zXO.o
3)不加-nostdlib编译与链接的过程:
第一步:
/usr/lib/gcc/i486-linux-gnu/4.3.2/cc1 -E -lang-asm -quiet -v basic.S -mtune=generic -fno-directives-only -o /tmp/ccsjdQ1s.s
第二步:
as -V -Qy -o /tmp/cc6h5zXO.o /tmp/ccsjdQ1s.s
第三步:
/usr/lib/gcc/i486-linux-gnu/4.3.2/collect2 --eh-frame-hdr -m elf_i386 --hash-style=both -dynamic-linker /lib/ld-linux.so.2 -o basic /usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib/crt1.o /usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib/crti.o /usr/lib/gcc/i486-linux-gnu/4.3.2/crtbegin.o -L/usr/lib/gcc/i486-linux-gnu/4.3.2 -L/usr/lib/gcc/i486-linux-gnu/4.3.2 -L/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib -L/lib/../lib -L/usr/lib/../lib -L/usr/lib/gcc/i486-linux-gnu/4.3.2/../../.. /tmp/cccfpbFe.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/i486-linux-gnu/4.3.2/crtend.o /usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib/crtn.o
注意:不加-nostdlib进行编译会报错的.
对比结果可以看出在collect2的第三步是不同的
4)什么是collect2
collect2是ld链接器的一个封装,最终还是要调用ld来完成链接工作,collect2的作用是在实现main函数的代码前对目标文件中命名的特殊符号进行收集.
这些特殊的符号表明它们是全局构造函数或在main前执行,collect2会生成一个临时的.c文件,将这些符号的地址收集成一个数组,然后放到这个.c文件里面,编译后与其他目标文件一起被链接到最终的输出文件中。
在这里我们没有加-nostdlib,所以自然不会调用__main,也就不会链接main函数所需引用的目标文件,也就不会对那些特殊的符号进行收集.
5)不加-nostdlib为什么会报错?
以下是不加-nostdlib的报错信息:
gcc -o basic basic.S
/tmp/ccaNEi0C.o: In function `_start':
(.text+0x0): multiple definition of `_start'
/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib/crt1.o:(.text+0x0): first defined here
/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib/crt1.o: In function `_start':
(.text+0x18): undefined reference to `main'
collect2: ld returned 1 exit status
一共有两个错误
第一个错误:
(.text+0x0): multiple definition of `_start'
它告诉我们,有多个地方引用了_start,我们只是在一个地方指定了_start的引用,另一个引用在哪里?
答案是在/usr/lib/crt1.o里面:
查看crt1.o的符号表:
nm /usr/lib/crt1.o
00000000 R _IO_stdin_used
00000000 D __data_start
U __libc_csu_fini
U __libc_csu_init
U __libc_start_main
00000000 R _fp_hw
00000000 T _start
00000000 W data_start
U main
看到这里是先引用了_start,才调用了main
OK,为什么在加-nostdlib进行编译时没有这个错误呢,因为加-nostdlib是不会链接crt1.o,也就不会收集它的符号表,自然不会报错.
如果在汇编程序将程序的入口点用别的名字呢?
编译的结果是不会报这个错.
例如:
gcc -o basic basic.S
/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib/crt1.o: In function `_start':
(.text+0x18): undefined reference to `main'
collect2: ld returned 1 exit status
而(.text+0x18): undefined reference to `main'错误又是怎么回事呢?
那是因为没加-nostdlib,它依然是链接了crt1.o,也就是收集了它的符号表,符号表里依然有main的调用,而汇编程序中没有main函数,所以自然会报错.
这就是引发第二个错误的原因.
三)用stract命令分析这个程序
命令如下:
strace -t ./basic
07:27:56 execve("./basic", ["./basic"], [/* 17 vars */]) = 0
07:27:56 nanosleep({5, 0}, NULL) = 0
07:28:01 _exit(0) = ?
这里涉有三个系统调用:
1)execve("./basic", ["./basic"], [/* 17 vars */]) = 0
execve是在父进程中fork一个子进程,在子进程中调用exec函数启动新的程序.
exec函数一共有六个,其中execve为内核级系统调用,其他(execl,execle,execlp,execv,execvp)都是调用execve的库函数。
2)nanosleep({5, 0}, NULL) = 0
是我们汇编程序中
movl $SYS_nanosleep, %eax
movl $sleeptime, %ebx
int $0x80
的系统调用,意为程序的睡眠时间.
3)_exit(0) = ?
是我们汇编程序中
movl $SYS_exit, %eax
movl $0, %ebx
int $0x80
的系统调用,意为退出程序.
这三个系统调用(syscall)就使程序从用户模式转向内核模式,而strace命令是会根踪内核模式的系统调用,并将结果打印输出.