不一样的hello world

Linux 的系统调用

  • 通过glibc提供的库函数

    glibc 是 Linux 下使用的开源的标准 C 库,它是 GNU 发布的 libc 库,即运行时库。glibc 为程序员提供丰富的 API,除了例如字符串处理、数学运算等用户态服务之外,最重要的是封装了操作系统提供的系统服务,即系统调用的封装。
    
  • 使用syscall直接调用

    该函数定义在 unistd.h 头文件中,函数原型如下:
    
    long int syscall (long int sysno, ...)
    
    1. sysno 是系统调用号,每个系统调用都有唯一的系统调用号来标识。
    2. ... 为剩余可变长的参数,为系统调用所带的参数。
    3. 返回值 该函数返回值为特定系统调用的返回值,在系统调用成功之后你可以将该返回值转化为特定的类型,如果系统调用失败则返回 -1。
    
  • 通过int指令陷入

    用户态程序通过软中断指令`int 0x80` 来陷入内核态,参数的传递是通过寄存器,eax 传递的是系统调用号,ebx、ecx、edx、esi和edi 来依次传递最多五个参数,当系统调用返回时,返回值存放在 eax 中。
    【系统调用号可在 /usr/include/asm/unistd_32.h(unistd_64.h)中查询 】
    

系统调用函数

write 调用

ssize_t write(int fd, const void *buf, size_t count);

write 调用的调用号为4,eax=4
fd 表示被写入的文件句柄,这里要向终端输出,文件句柄为1,ebx=1
buf 表示要写入的缓冲区地址,ecx=str 【hello world!字符串】
size 表示要写入的字节数,edx=13  【hello world!】

exit 调用

void exit(int status);

exit 调用的调用号为1。
status 表示进程退出码,如我们平时的main程序return的数值会返回给系统库,由系统库将该数值传递给exit系统调用。

有了上面的铺垫我们就可以做到,写一个不一样的hello world 程序,并且做到以下要求:

  • 不调用任何系统库
  • 不使用main函数【结束进程使用系统调用函数exit】

代码实现

GCC/GNU 编译器和 Clang/LLVM 编译器默认使用 AT&T/UNIX 汇编语法, GCC 可以通过加参数 -masm=intel 来使用 Intel 汇编语法,该参数并不适用于 Clang。

这里使用 Intel 语法 【AT&T/UNIX 语法 相应修改即可】

/* helloworld.c */   

char * str="hello world\n";

void print(){

	__asm(
	"mov edx,13\n\t"     //字符串长度
	"mov ecx,str\n\t"   //待显示字符串 
	"mov ebx,1\n\t"      //文件描述符(stdout)
	"mov eax,4\n\t"      //write 系统调用号 
	"int 0x80\n\t"       //软中断指令 进入系统调用 
	);
}


void end(){

	__asm(
	"mov ebx,0\n\t"     //进程退出码 
	"mov eax,1\n\t"     //exit 系统调用号
	"int 0x80\n\t"
	);
}

void run()
{
	print();
	end();
}

进行编译

# -masm=intel Intel 语法
# -fno-builtin 关闭gcc内置函数功能
# 生成可重定向文件 

gcc helloworld.c -c -masm=intel -fno-builtin 

# -static 让ld使用静态链接方式链接程序
# -e 程序入口
# 生成可执行文件

ld -static -e run -o helloworld helloworld.o

image

image

可以看到成功生成可执行文件!

.text 保存的是程序的指令,它是只读的。
.rodata 保存的是字符串“HelloWorld!\n”,它也是只读的。
.data 保存的是Sir全局变量,看上去它是可读写的,但我们并没有在程序中改写该变量,所以实际上它也是只读的。
.comment保存的是编译器和系统版本信息,这些信息也是只读的。由于.comment里面保存的数据并不关键,对程序的运行没有作用,所以可以丢弃。

.eh_frame 可以通过把代码写入.eh_frame中(覆盖其原来的内容)可以实现binary大小基本没有变化。若存在该段,我们能够进行改写并无影响。
【但这个段也极大增加了elf文件的大小】

鉴于这些段的属性如此相似,原则上讲,我们可以把它们合并到一个段里面,该段的属性是可执行、可读的,包含程序的数据和指令。为了达到这个目的,我们使用Id链接脚本来控制链接过程。

ld链接脚本
/* tinyhelloworld.lds */

ENTRY(run)

SECTIONS
{
	. = SIZEOF_HEADERS;  /* = 旁的空格很关键,不然也会报错 */
	.tinytext : {*(.text) *(.rodata) *(.data)}
	/DISCARD/ : {*(.comment) *(.eh_frame)}
}

# "."表示当前虚拟地址
# SIZEOF_HEADERS 输出文件头的大小
# tinytext: 新合并段名称
# /DISCARD/: 丢弃其中段

链接命令

ld -static -T tinyhelloworld.lds -o tinyhelloworld helloworld.o -s 

# 注意:这里 -T -o 的顺序不当,会引起报错
# -s 去除符号

image

可以看到,除了重定向表外,就只有.tinytext段了。并且可执行程序的大小为576字节