《程序员的自我修养》学习笔记——不一样的hello world【第四弹】
不一样的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
可以看到成功生成可执行文件!
.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 去除符号
可以看到,除了重定向表外,就只有.tinytext段了。并且可执行程序的大小为576字节。