相传,每一个程序员学习每一门语言的时候,都会先做同一件事情—在某种输出终端上打印出“Hello world!”。鉴于这种做法是如此深入人心,所以我也想在开始自己的博客时延续这一传统,用C语言来输出“Hello world!”,而为了体现现在和四年前在学校上C语言课时的不同,这一段程序将不使用C依赖库,转而使用内嵌汇编来实现必要的底层操作,并对其中(我觉得)有意思的地方作出讨论。
代码如下
char * str = "Hello world!\n";
void print()
{
asm(
"movl $13, %%edx \n\t"//string lenght:13
"movl %0, %%ecx \n\t" //
"movl $0, %%ebx \n\t" //stdout:0
"movl $4, %%eax \n\t" //write system call: 4
"int $0x80 \n\t"
::"r"(str)
);
}
void exit()
{
asm(
"movl $42, %ebx \n\t"
"movl $1, %eax \n\t"
"int $0x80 \n\t"
);
}
void mymain()
{
print();
exit();
}
这里使用了一些AT&T汇编(对,我是在Linux下写的),内嵌实现了两个函数printf
和exit
来被mymain
调用,由于没有使用GLIBC等C库(不知道你注意到没有,这一段小程序也没有main),也就没有start.S 来调用main函数,所以需要在链接时指定入口函数。两种指定方法在makefile中如下所示:
C_FLAGS := -c -fno-builtin
LD_FLAGS := -static -e mymain
#LD_FLAGS := -static -T hello_world.lds
TARGET = hello_world
$(TARGET):
gcc $(C_FLAGS) hello_world.c
ld $(LD_FLAGS) -o $@ hello_world.o
clean:
rm *.o $(TARGET)
分别可以使用编译选项-e mymain
和链接脚本-T hello_world.lds
来实现,链接脚本中指定入口函数的相关语句是ENTRY(mymain)
,在此不再赘述。如果使用这部分代码和makefile可以编译通过的话,你应该会得到一个很小而且不依赖动态装载的ELF文件;如果编译不过,请检查你的Linux系统是否是64位的,碰巧是的话,在gcc和ld时分别加上-m32
和-m elf_i386
也许可以解决问题。
一些讨论
至此,已经实现了一个比较简单的程序来输出那句耳熟能详的话,对于其中的一些较深层次的东西,还想做出一些讨论。现在是自由发挥时间,所以会想到哪写到哪。
1. 代码的最开始,声明了一个字符串并指向str
,作为一个已初始化的全局变量,会保存在ELF的.data段中。为了证明这一点,使用objdump -D hello_world
会得到下面的结果,其中的mov 0x8049160,%eax
将字符串的地址传递给系统调用。
...
8048095: 89 e5 mov %esp,%ebp
8048097: 53 push %ebx
8048098: a1 60 91 04 08 mov 0x8049160,%eax
804809d: ba 0d 00 00 00 mov $0xd,%edx
80480a2: 89 c1 mov %eax,%ecx
...
Disassembly of section .data:
08049160 <str>:
8049160: d6 (bad)
8049161: 80 .byte 0x80
8049162: 04 08 add $0x8,%al
此时如果不退出函数(exit();
前加上while(1);
。当然,实现sleep
函数是比较好的做法,但是要包含signal.h,作为一个强迫症~),后台运行此程序记住PID号并执行cat /proc/xxxx/maps
,会看到08049000-0804a000 rw-p 00000000 08:01 950559
这样的信息,代表这个变量保存在可读可写段。为了提高内存页的使用效率,将可执行文件装载到进程虚拟内存空间时权限相同的段将会一起映射,所以现在也许不应该再称之为Section(段),而是Segment(节)。而如果将这个字符串的声明由char * str
改为char * const str
(使用const char * str
会如何呢),使用和刚刚相同的方法查看ELF文件结构和进程内存分布会发现,运行时这个字符串将会和代码段一起保存在类似08048000-08049000 r-xp 00000000 08:01 950559
的虚拟内存地址中,虽然str
并不具有可执行权限。
2. print();
函数,通过软中断指令int $0x80
来进入内核态调用4号(sys_write)系统调用,往标准输出stdout:0
写数据来实现打印。都是比较普通的做法,在此不再赘述。
3. exit();
函数,与上面类似,调用1号(sys_exit)系统调用来退出程序。