We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
本文使用gcc扩展汇编写实现一个不依赖C标准库glibc的HelloWorld程序,并简单介绍链接控制脚本的使用。
我们知道glibc库的printf函数最终是使用write系统调用向终端输出字符,既然我们想编写一个不依赖运行时库的最"小"的HelloWorld程序,那么我们就得自己来进行系统调用
在x86架构可以使用0x80号中断(int 0x80)来进行系统调用 在x86_64架构除了使用0x80号中断以外,还有一个专用的指令syscall可以进行系统调用(更加高效),
int 0x80
syscall
两种方式实现如下,需要用到gcc扩展汇编知识,可以参考这里
char *str = "Hello world!\n"; void print() { asm("movl $13, %%edx \n\t" "movl %0, %%ecx \n\t" "movl $1, %%ebx \n\t" "movl $4, %%eax \n\t" "int $0x80 \n\t" ::"r"(str):"edx", "ecx", "ebx", "esi"); } void exit() { asm("movl $42, %ebx \n\t" "movl $1, %eax \n\t" "int $0x80 \n\t"); } void nomain() { print(); exit(); }
int 0x80系统调用的参数传递和返回值规则如下
write系统调用的C语言描述如下
int write(int filedesc, char *buffer, int size)
进行write系统调用的汇编代码意义如下 movl $13, %%edx 将字符串长度传递给第3个参数size movl %0, %%ecx 将指针str传递给第二个参数buff(%0指代str变量) movl $1, %%ebx 将立即数1传递给第一个参数filedesc(标准输出的文件描述符为1) movl $4, %%eax 寄存器eax用来存放系统调用号,write调用号为4,见unistd_32.h "r"(str)表示由编译器决定使用哪个通用寄存器来存放str变量 "edx", "ecx", "ebx", "esi" 告知编译器汇编代码会修改这几个寄存器的值
movl $13, %%edx
movl %0, %%ecx
movl $1, %%ebx
movl $4, %%eax
"r"(str)
"edx", "ecx", "ebx", "esi"
详细语法规则见gcc扩展汇编知识
exit系统调用原理一致,就不赘述了
接下来进行编译链接,由于该代码是参考《链接,装载与库》书中的示例代码,且书中系统环境为32位,而本机环境为64位,所以编译和链接时需要指定使用32位(-m32选项和-m elf_i386)
-m32
-m elf_i386
$ gcc -m32 -c -fno-builtin tinyhelloworld.c $ ld -static -e nomain -m elf_i386 -o tinyhelloworld. tinyhelloworld.o $ ./tinyhelloworld. hello world!
-fno-builtin选项关闭gcc的内置函数功能,避免程序中某些函数被替换 -m32选项指定编译出32位的目标文件
-fno-builtin
-m elf_i386选项指定链接的的是32位程序 -e nomain设置程序的入口函数为nomain
-e nomain
如果编译时不使用-m32选项,那么会报错如下
$ gcc -c -fno-builtin tinyhelloworld.c tinyhelloworld.c: Assembler messages: tinyhelloworld.c:6: Error: unsupported instruction `mov'
报错行是movl %0, %%ecx
错误原因可能如下:由于代码使用r来约束变量str,表示由编译器来决定使用任意通用寄存器,由于本机环境为64位,所以编译器使用了一个64位的寄存器来存放str,因此该指令将一个64位寄存器赋值给32位寄存器ecx是错误的。参考What does the GCC error message, “Error: unsupported for `mov'”, mean?
r
那如果还是想编译成64位程序而不是32位程序可以如何做呢?
既然编译器使用64位寄存器来存放str会报错,那我们只要不让str存储在寄存器即可,这里我尝试使用内存来存放变量str,发现结果可行,具体修改为"r"(str) to "m"(str)
"m"(str)
重新编译链接,运行,成功!
$ gcc -c -fno-builtin tinyhelloworld.c $ ld -static -e nomain -o tinyhelloworld tinyhelloworld.o $ ./tinyhelloworld hello world!
char *str = "Hello World!\n"; void print() { asm("movq $1, %%rax \n\t" "movq $1, %%rdi \n\t" "movq %0, %%rsi \n\t" "movq $13, %%rdx \n\t" "syscall" : :"r"(str) :"rax", "rdi", "rsi", "rdx"); } void exit() { asm("movq $60, %rax \n\t" "movq $42, %rdi \n\t" "syscall"); } void nomain() { print(); exit(); }
编译,链接,运行
syscall系统调用的参数传递和返回值规则如下
调用规则与int 0x80类似,值得注意的是syscall和int 0x80使用的系统调用号不一致,syscall的系统调用号定义在linux/arch/x86/entry/syscalls/syscall_64.tbl,int x80的系统调用号定义在linux/arch/x86/entry/syscalls/syscall_32.tbl
接下来介绍如何使用链接控制脚本来控制链接器ld的链接过程
链接控制脚本就是描述输入文件的section如何变成输出文件中的section,并且控制输出文件的内存布局
当我们在链接程序时如果没有指定链接脚本,ld会使用一个默认的链接脚本,可以使用$ld -verbose查看,默认链接脚本都存放在/usr/lib/ldscripts下,不同机器平台,输出文件格式都有相应的链接脚本。
$ld -verbose
/usr/lib/ldscripts
下面使编写个最简单的链接控制脚本
ENTRY(nomain) SECTIONS { . = 0x400000 + SIZEOF_HEADERS; tinytext : {*(.text) *(.data) *(.rodata)} /DISCARD/ : {*(.comment)} }
该链接脚本指定了如下规则
ENTRY(nomain)
. = 0x400000 + SIZEOF_HEADERS
tinytext : {*(.text) *(.data) *(.rodata)}
/DISCARD/ : {*(.comment)}
链接时使用-T指定使用的链接脚本 $ ld -static -T tinyhelloworld.lds -o tinyhelloworld tinyhelloworld.o
-T
$ ld -static -T tinyhelloworld.lds -o tinyhelloworld tinyhelloworld.o
链接脚本如何编写的细节这里不再细究,这里介绍下学习过程中遇到的一个比较疑惑的问题
在32位机器上设置程序起始虚拟地址为0x400000 + SIZEOF_HEADERS,而在64位机器上设置为0x08048000 + SIZEOF_HEADERS
为什么选择0x400000和0x08048000这两个值呢?
通过阅读Why is address 0x400000 chosen as a start of text segment in x86_64 ABI?, Why do virtual memory addresses for linux binaries start at 0x8048000?, Why is the ELF execution entry point virtual address of the form 0x80xxxxx and not zero 0x0?和On Linux, why does the text segment start at 0x08048000? What is stored below that address?这几个回答了解一二,总结如下:
对该问题理解总结得比较粗浅,有时间可以仔细研究下上面的那几个回答,理解更多细节
The text was updated successfully, but these errors were encountered:
No branches or pull requests
本文使用gcc扩展汇编写实现一个不依赖C标准库glibc的HelloWorld程序,并简单介绍链接控制脚本的使用。
我们知道glibc库的printf函数最终是使用write系统调用向终端输出字符,既然我们想编写一个不依赖运行时库的最"小"的HelloWorld程序,那么我们就得自己来进行系统调用
在x86架构可以使用0x80号中断(
int 0x80
)来进行系统调用在x86_64架构除了使用0x80号中断以外,还有一个专用的指令
syscall
可以进行系统调用(更加高效),两种方式实现如下,需要用到gcc扩展汇编知识,可以参考这里
int 0x80
int 0x80
系统调用的参数传递和返回值规则如下write系统调用的C语言描述如下
进行write系统调用的汇编代码意义如下
movl $13, %%edx
将字符串长度传递给第3个参数sizemovl %0, %%ecx
将指针str传递给第二个参数buff(%0指代str变量)movl $1, %%ebx
将立即数1传递给第一个参数filedesc(标准输出的文件描述符为1)movl $4, %%eax
寄存器eax用来存放系统调用号,write调用号为4,见unistd_32.h"r"(str)
表示由编译器决定使用哪个通用寄存器来存放str变量"edx", "ecx", "ebx", "esi"
告知编译器汇编代码会修改这几个寄存器的值详细语法规则见gcc扩展汇编知识
exit系统调用原理一致,就不赘述了
接下来进行编译链接,由于该代码是参考《链接,装载与库》书中的示例代码,且书中系统环境为32位,而本机环境为64位,所以编译和链接时需要指定使用32位(
-m32
选项和-m elf_i386
)-fno-builtin
选项关闭gcc的内置函数功能,避免程序中某些函数被替换-m32
选项指定编译出32位的目标文件-m elf_i386
选项指定链接的的是32位程序-e nomain
设置程序的入口函数为nomain如果编译时不使用
-m32
选项,那么会报错如下报错行是
movl %0, %%ecx
错误原因可能如下:由于代码使用
r
来约束变量str,表示由编译器来决定使用任意通用寄存器,由于本机环境为64位,所以编译器使用了一个64位的寄存器来存放str,因此该指令将一个64位寄存器赋值给32位寄存器ecx是错误的。参考What does the GCC error message, “Error: unsupported for `mov'”, mean?那如果还是想编译成64位程序而不是32位程序可以如何做呢?
既然编译器使用64位寄存器来存放str会报错,那我们只要不让str存储在寄存器即可,这里我尝试使用内存来存放变量str,发现结果可行,具体修改为
"r"(str)
to"m"(str)
重新编译链接,运行,成功!
syscall
编译,链接,运行
syscall
系统调用的参数传递和返回值规则如下调用规则与int 0x80类似,值得注意的是syscall和int 0x80使用的系统调用号不一致,syscall的系统调用号定义在linux/arch/x86/entry/syscalls/syscall_64.tbl,int x80的系统调用号定义在linux/arch/x86/entry/syscalls/syscall_32.tbl
接下来介绍如何使用链接控制脚本来控制链接器ld的链接过程
链接控制脚本就是描述输入文件的section如何变成输出文件中的section,并且控制输出文件的内存布局
当我们在链接程序时如果没有指定链接脚本,ld会使用一个默认的链接脚本,可以使用
$ld -verbose
查看,默认链接脚本都存放在/usr/lib/ldscripts
下,不同机器平台,输出文件格式都有相应的链接脚本。下面使编写个最简单的链接控制脚本
该链接脚本指定了如下规则
ENTRY(nomain)
指定程序的入口为nomain函数. = 0x400000 + SIZEOF_HEADERS
将当前虚拟地址设置成0x400000 + SIZEOF_HEADERS,SIZEOF_HEADERS为输出文件的文件头大小tinytext : {*(.text) *(.data) *(.rodata)}
将所有输入文件的.text, .data和.rodata段合并到输出文件的.tinytext/DISCARD/ : {*(.comment)}
将所有输入文件中的.comment段丢弃,不保存到输入文件中链接时使用
-T
指定使用的链接脚本$ ld -static -T tinyhelloworld.lds -o tinyhelloworld tinyhelloworld.o
链接脚本如何编写的细节这里不再细究,这里介绍下学习过程中遇到的一个比较疑惑的问题
在32位机器上设置程序起始虚拟地址为0x400000 + SIZEOF_HEADERS,而在64位机器上设置为0x08048000 + SIZEOF_HEADERS
为什么选择0x400000和0x08048000这两个值呢?
通过阅读Why is address 0x400000 chosen as a start of text segment in x86_64 ABI?, Why do virtual memory addresses for linux binaries start at 0x8048000?, Why is the ELF execution entry point virtual address of the form 0x80xxxxx and not zero 0x0?和On Linux, why does the text segment start at 0x08048000? What is stored below that address?这几个回答了解一二,总结如下:
对该问题理解总结得比较粗浅,有时间可以仔细研究下上面的那几个回答,理解更多细节
The text was updated successfully, but these errors were encountered: