x64汇编之从程序编辑到系统调用
大家好,你们可以叫我凌,是个16岁的网络安全学习者。
今天我们来学习x64汇编的调试、结构以及系统调用,内容重要且基础。那我们就直接开始吧!
注:本教程采用使用范围更广的NASM语法,如需学习其他语法自行上网查找两种语法的区别即可。只因语法不同,因此花几十分钟即可快速掌握。
你的首个asm程序
按照各个语言的传统,我们先来简单写段“hello word”的程序,该文章也会从这个程序下手进行讲解,以帮助读者更好地了解。
打开终端,输入以下命令以此来创建并编写汇编程序(如不理解或许应该去补充下Linux的知识)
vim hello.asm
section .data msg db "hello, world",0 section .bss section .text global main main: mov rax, 1 ; 1表示写入 mov rdi, 1 ; 1表示标准输出 mov rsi, msg ; 需要显示的是字符串存放在rsi中 mov rdx, 12 ; 字符串的长度,不包括0 syscall ; 显示字符串 mov rax, 60 ; 60表示退出 mov rdi, 0 ; 0是成功退出代码 syscall ; 退出随后点击Esc,输入:wq保存并退出编辑模式
这就是串简单的汇编代码,接下来我们要对其进行汇编和使用gcc链接达到运行的效果
汇编
汇编就是生成目标文件,使用以下命令来执行
各个部分解释如下
nasm
调用 NASM(Netwide Assembler)汇编器。
-f elf64
指定输出文件的格式为elf64,即 64 位的 ELF(Executable and Linkable Format)。这是 Linux 系统上常用的目标文件格式,用于后续链接生成可执行文件。
hello.asm
输入的汇编源文件(通常包含 64 位 x86 汇编代码)。
-o hello.o
指定输出的目标文件名为hello.o。如果不加-o,默认输出文件名会是hello.o
这样 home目录下就会多出相关的文件(刚刚生成的)
使用gcc链接
这是是在Linux下使用GCC将之前生成的hello.o目标文件链接成最终的可执行文件hello
这里要注意-no-pie选项,这样就可以生成可执行文件了
以下为各部分解释
gcc
调用 GCC 编译器驱动,这里主要用于链接阶段(因为输入是.o目标文件)。
-no-pie现代 Linux 默认生成PIE(位置无关可执行文件),程序加载地址随机化,安全性更高。
加
-no-pie后,程序会被链接到固定基址(如0x400000),代码中可以直接使用绝对地址。
hello.o
输入的目标文件,由之前的nasm -f elf64 hello.asm -o hello.o生成。
-o hello
指定输出的可执行文件名为hello(默认是a.out)。
注:是 GCC 链接时的一个选项,作用是生成传统、非位置无关的可执行文件。当手写汇编代码(如 NASM)没有使用RIP 相对寻址,而是直接写了绝对地址或依赖固定内存布局时用默认 PIE 模式链接会出错,此时加上-no-pie可以解决兼容性问题。这里进行了解即可
运行代码
接下来我们使用以下命令即可运行我们刚刚创建的可执行文件
汇编程序的结构
刚刚程序我们已经见识的汇编程序的模样,以下为汇编程序的主要组成部分:
.data段 .bss段 .text段
接下来我们将逐一讲解各个部分
.data段
.data段(section)就是准备好要处理的数据,使用以下格式声明和定义初始化数据
<变量名称> <类型> <值>
如该段包含变量,当源代码被汇编并链接到可执行文件时,将为该变量分配内存。变量名称为符号名称,对内存位置和变量的引用可以占用一个或多个内存位置。变量名称是指变量在内存中的起始位置。
在上面程序中.data段包含变量msg,这是个符号名称,指向内存地址h,为字符串“hello word”的第一个字节。字符串是内存中字符的“列表”或“数组”。
事实上,内存中的任何连续列表都可以视为字符串
另外,我们发现这后面有个0。你可以忽略或不写他,但是带来的后果得自负。这里的0并不是ASCII里面的0,而是单纯的数字0(添加一个空字节),并且它的位置包含一个字节
此外,该段还可以包含常量,学过其他语言的应该知道,即无法更改的值,下面为声明格式
<常量名称> equ <值>
例如:abc equ 12345
.bss段
.bss段就是写出处理数据的步骤。用于存放未初始化的量,未初始化变量的空间在此段声明
<变量名称> <类型> <数字>
以下为bss段的数据类型
例如以下内容声明个包含20个双字的数组
dArray resd 20该段的变量不包含任何值,这些值将在执行中分配。内存位置不是在编译时保留的,而是在执行时保留的。
当程序开始执行时,程序会从操作系统中请求所需的内存,分配给.bss段中变量并初始化为零。如果执行时没有足够的内存可用于.bss变量,程序将崩溃
.txt段
.txt段是所有操作所在的位置
global main main:main:称为标签,后面最好跟上冒号“:”避免引起不必要的麻烦!
此外我们还会发现有许多数字,这称为系统调用码,不同数字在不同寄存器里面有不同的意思。比如下面这串代码就是写入的意思(有关寄存器的内容见x64汇编:内存工作的基本原理,之后会出更详细的寄存器文章)
mov rax,1以下为mov的指令用法(极为重要!)
mov 寄存器,即时值
mov 寄存器,内存
mov 内存,寄存器
注:mov 内存,内存 为不合法的!
系统调用
什么是系统调用
我们的程序运行在“用户态”,不能直接操作硬件(如屏幕、键盘)。需要操作系统内核帮忙时,通过“系统调用”请求内核执行。syscall指令就是触发这个请求的开关。
系统调用的三个步骤
1.rax中放服务号(告诉内核做什么)
2.rdi, rsi, rdx, r10, r8, r9中放参数(告诉内核具体怎么做,最多6个)
3. 执行syscall
常用服务号(rax)
| 服务号 | 名称 | 功能 |
| 1 | sys_write | 写入数据(如输出到屏幕) |
| 60 | sys_exit | 退出程序 |
参数寄存器
| 参数位置 | 寄存器 |
| 第1个 | rdi |
| 第2个 | rsi |
| 第3个 | rdx |
| 第4个 | r10 |
| 第5个 | r8 |
| 第6个 | r9 |
注:超过6个参数时通过栈传递,但入门极少遇到。
易混淆点澄清
1.rax=1是“写入”服务;rdi=1在sys_write中是“屏幕”,在sys_exit中是“退出码1(错误)”。同一个数字在不同寄存器/不同调用中含义不同。
2.只有sys_exit时rdi才叫“退出码”;在sys_write中叫“文件描述符”
