不一样的hello world

一. 简介

  最近学习了一些C内联汇编的知识,简单的尝试了一下,发现里面有很多有趣的东西,分享如下。首先说明,本文的内联使用仅为一个小例子,并不规范,另外32位和64位有着较大区别,本文基于64位Ubuntu16.04环境编译。

二. 源码介绍

  代码本身很简单,包括了三个函数,分别用于打印,退出以及作为函数入口,实际效果为输出Hello world

2.1 print()

  打印函数printf()本身其实是调用了write()函数(也可以调用puts()等),这里通过80中断直接调用write()对应的系统调用。write()的API如下所示

1
int write(int fd, char *buffer, int size);
  • 由于系统调用号(查中断标可得)为4,所以直接传递给rax赋值4即可。
  • fd即文件描述符,这里使用标准输出,即stdout,所以fd为0,rbx赋值为0
  • buffer即文件缓冲区,这里就是str字符串,使用rcx传递,赋值为地址对应的变量即0地址
  • size表示的时文件字节数,这里是13字节,因此rdx赋值为13

2.2 exit()

  exit()对应的系统调用为exit(),系统调用号为1,所以rax赋值为1。rbx表示返回值,这里42其实是一个神奇的数字,有兴趣的可以看一看《银河系漫游指南》(大雾)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//hello.c
char* str = "Hello world!\n";

void print()
{
asm(
"mov $13, %%rdx \n\t"
"mov %0, %%rcx \n\t"
"mov $0, %%rbx \n\t"
"mov $4, %%rax \n\t"
"int $0x80 \n\t"
::"r"(str):"rdx","rcx","rbx");
}

void exit()
{
asm(
"mov $42, %rbx \n\t"
"mov $1, %rax \n\t"
"int $0x80 \n\t"
);
}

void hello()
{
print();
exit();
}

  注意,本代码为64位编译环境,如果是32位需要改为movl配合ebx/ecx系列或者编译时加上-m32选项。

三. 编译和执行

  使用如下命令编译和链接,生成可执行文件。这里-fno-builtin表示不进行编译器优化。否则对于纯字符串的printf(),编译器会自动优化为使用puts()函数。另外链接时,-e hello表示入口函数为hello(),而非常见的main()函数。

1
2
gcc -c -fno-builtin hello.c
ld -static -e hello -o hello hello.o

  运行hello可见如下结果,输出成功,返回值为预设的42。

1
2
3
4
$ ./hello
Hello world!
$ echo $?
42

四. 自制lds

  通过objdump可以看到hello包括了.data, .text, .rodata等字段,同时还有.comment。这里我们尝试自己制作链接脚本,使这三个字段合并,同时去掉.comment字段,这样子应该可以让可执行文件的大小大大缩减。

  • ENTRY表明了入口函数为hello函数
1
2
3
4
5
6
7
8
9
10
ENTRY(hello)

SECTIONS
{
. = 0X00000000 + SIZEOF_HEADERS;

tinytext : {*(.text) *(.data) *(.rodata)}

/DISCARD/ : {*(.comment)}
}

  再次编译

1
ld -static -T hello.lds -o hello hello.o

  编译完依然可以正常执行,通过objdump可见已经生效,可执行文件的大小从1.6KB成功缩减到704B,所以就算是成功了。实际还可以通过-s再次减小大小。

总结

  本文基于中断,汇编,编译,链接等知识,进行一次综合性的上手小实验,适合新人们做为入门级小尝试来步入学习。另外内联汇编本身较为复杂,本文进行了大量简化,实际在源码及工业级运用会需要考虑更多的安全性等因素,同时80中断实际并不适合在64位使用,这个回头会再写一篇文章专门叙述。鉴于水平有限,还望大佬们多多指点不足之处,共同进步。

坚持原创,坚持分享,谢谢鼓励和支持