一、代码示例1
1.1 代码说明
.section .data
message:
.string "Hello, World!\n"
len = . - message
.section .text
.globl _start
_start:
# 调用 write() 函数输出 "Hello, World!"
mov $1, %rax # 系统调用号为 1 表示 write()
mov $1, %rdi # 文件描述符为 1 表示标准输出
lea message(%rip), %rsi # 输出的字符串地址
mov $len, %rdx # 输出的字符串长度
syscall # 调用系统调用
# 调用 exit() 函数退出程序
mov $60, %rax # 系统调用号为 60 表示 exit()
xor %rdi, %rdi # 返回值为 0
syscall # 调用系统调用
这段汇编代码是在标准输出上输出 “Hello, World!”,然后退出程序。
as -o hello.o hello.s
ld -o hello hello.o
# ./hello
Hello, World!
首先,在 .data 段中定义了一个名为 message 的字符串,内容为 “Hello, World!\n”。.data 段用于定义程序中的静态数据,这些数据在程序运行期间不会被修改。
接下来,在 .text 段中定义了一个全局标号 _start,这是程序的入口点。.text 段用于定义程序的代码。
在 _start 标号下,使用汇编指令依次完成以下操作:
(1)将系统调用号 1(表示 write() 函数)存储到 %rax 寄存器中。
(2)将文件描述符 1(表示标准输出)存储到 %rdi 寄存器中。
(3)使用 lea 指令将字符串 message 的地址存储到 %rsi 寄存器中。lea 指令可以计算一个地址,并将计算结果存储到目标寄存器中。
(4)将字符串的长度 len 存储到 %rdx 寄存器中。
(5)使用 syscall 指令调用系统调用,将输出字符串 “Hello, World!\n” 到标准输出。
(6)接下来,将系统调用号 60(表示 exit() 函数)存储到 %rax 寄存器中,将返回值 0 存储到 %rdi 寄存器中,然后使用 syscall 指令调用系统调用,退出程序。
NAME
write - write to a file descriptor
SYNOPSIS
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
DESCRIPTION
write() writes up to count bytes from the buffer pointed buf to the file referred to by the file descriptor fd.
fd(文件描述符) | 描述 |
---|---|
文件描述符0:标准输入(stdin) | 文件描述符0通常用于标准输入,也就是从键盘或者其他标准输入设备读取数据。当进程从标准输入读取数据时,实际上是从文件描述符0所表示的输入流中读取数据。 |
文件描述符1:标准输出(stdout) | 文件描述符1通常用于标准输出,也就是向屏幕或者其他标准输出设备输出数据。当进程向标准输出写入数据时,实际上是向文件描述符1所表示的输出流中写入数据。 |
文件描述符2:标准错误输出(stderr) | 文件描述符2通常用于标准错误输出,也就是向屏幕或者其他标准错误输出设备输出错误信息。当进程向标准错误输出写入数据时,实际上是向文件描述符2所表示的输出流中写入数据。 |
寄存器 | 说明 | |
---|---|---|
rax | 系统调用号/返回值 | 系统调用号 = 1 表示 write() 函数 |
rdi | 第一个参数 | fd =1 表示标准输出 |
rsi | 第二个参数 | buf = message 字符串的地址 |
rdx | 第三个参数 | count = strlen(message) = len |
1.2 系统调用号
在 Linux x86_64 系统中,系统调用使用 syscall 汇编指令来执行。每个系统调用都有一个唯一的系统调用号(system call number),用于标识要执行的系统调用。这个系统调用号存储在 %rax 寄存器中,syscall 指令会根据 %rax 寄存器中的系统调用号来执行相应的系统调用。
# cat /usr/include/asm/unistd_64.h | grep write
#define __NR_write 1
# cat /usr/include/asm/unistd_64.h | grep exit
#define __NR_exit 60
1.3 lea 指令
lea 指令(Load Effective Address)可以计算出一个有效地址,并将其存储到指定的寄存器中。它的基本语法如下:
lea offset(base, index, scale), destination
(1)offset 是一个可选的常数偏移量,表示需要加到基址上的偏移量。如果没有常数偏移量,则可以省略这个部分。
(2)base 是一个可选的基址寄存器,表示需要加上偏移量的地址,可以是 64 位寄存器或内存地址。
(3)index 是一个可选的变址寄存器,表示需要加上偏移量的地址,可以是 64 位寄存器。
(4)scale 是一个可选的比例因子,表示变址寄存器的值需要乘上的比例因子,可以是 1、2、4 或 8。
(5)destination 是需要存储结果的寄存器,可以是 64 位寄存器。
lea 指令将根据给定的基址寄存器、变址寄存器、比例因子和常数偏移量计算出一个有效地址,并将其存储到目标寄存器中。例如,lea (%rax, %rbx, 4), %rdx 指令将计算出 %rax + 4 * %rbx 的值,并将结果存储到 %rdx 中。
在这个例子中,lea message(%rip), %rsi 指令使用了 %rip 寄存器作为基址寄存器,表示当前指令的地址。message 符号表示一个字符串常量,它的地址在程序的数据段中被定义。由于 %rip 寄存器存储的是当前指令的地址,因此 message(%rip) 表示当前指令的地址与 message 符号的地址之差。这个差值是一个 32 位的相对偏移量,它在编译时可以计算出来并存储在指令中。因此,这个指令的作用是将字符串 message 的地址与当前指令的地址相加,计算出字符串的有效地址,并将这个地址存储到 %rsi 寄存器中。
lea message(%rip), %rsi # 输出的字符串地址
这条指令的机器码是:48 8d 35 <address>,其中 <address> 是一个 32 位的相对地址,用于计算 message 字符串相对于指令地址的偏移量。具体来说,48 是一个前缀字节,用于指示这是一个 64 位指令;8d 是 lea 操作码;35 是 Mod R/M 字节,用于指示源操作数的地址模式和目标操作数的寄存器编号;<address> 是一个 32 位的相对地址,用于计算 message 字符串的地址。
<address> 的值是一个相对于下一条指令地址的偏移量,而不是一个绝对地址。
]# objdump -d hello.o
hello.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <_start>:
0: 48 c7 c0 01 00 00 00 mov $0x1,%rax
7: 48 c7 c7 01 00 00 00 mov $0x1,%rdi
e: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 15 <_start+0x15>
15: 48 c7 c2 0e 00 00 00 mov $0xe,%rdx
1c: 0f 05 syscall
1e: 48 c7 c0 3c 00 00 00 mov $0x3c,%rax
25: 48 31 ff xor %rdi,%rdi
28: 0f 05 syscall
e: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 15 <_start+0x15>
提取机器码:
objdump -d ./hello|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-7 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
# objdump -d ./hello1|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-7 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
"\x48\xc7\xc0\x01\x00\x00\x00\x48\xc7\xc7\x01\x00\x00\x00\x48\x8d\x35\x15\x00\x20\x00\x48\xc7\xc2\x0f\x00\x00\x00\x0f\x05\x48\xc7\xc0\x3c\x00\x00\x00\x48\x31\xff\x0f\x05"
二、代码示例2
.global _start
.section .data
input_buffer:
.skip 1024
prompt:
.ascii "Enter some text: "
prompt_len = . - prompt
.section .text
_start:
# 输出提示信息
movq $1, %rax # 系统调用号为 1 表示 write()
movq $1, %rdi # 文件描述符为 1 表示标准输出
leaq prompt(%rip), %rsi # 输出的字符串地址
movq $prompt_len, %rdx # 输出的字符串长度
syscall # 调用系统调用
# 读取用户输入
movq $0, %rax # 系统调用号为 0 表示 read()
movq $0, %rdi # 文件描述符为 0 表示标准输入
leaq input_buffer(%rip), %rsi # 存储输入的字符串地址
movq $1024, %rdx # 读取的最大字节数
syscall # 调用系统调用
# 输出用户输入
movq $1, %rax # 系统调用号为 1 表示 write()
movq $1, %rdi # 文件描述符为 1 表示标准输出
movq %rsi, %rsi # 输入的字符串地址
syscall # 调用系统调用
# 退出程序
movq $60, %rax # 系统调用号为 60 表示 exit()
xorq %rdi, %rdi # 返回值为 0
syscall # 调用系统调用
# as -o read.o read.s
# ld -o read read.o
# ./read
Enter some text: ni hao
ni hao
# cat /usr/include/asm/unistd_64.h | grep read
#define __NR_read 0
NAME
read - read from a file descriptor
SYNOPSIS
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
DESCRIPTION
read() attempts to read up to count bytes from file descriptor fd into the buffer starting at buf
三、代码示例3
3.1 代码演示
.section .text
.globl _start
_start:
# 调用 execve() 函数执行 /bin/sh 命令
xor %rax, %rax
movb $59, %al # 系统调用号为 59 表示 execve()
xor %rdi, %rdi # 参数 1,文件路径
movq $0x68732f6e69622f, %rdi # /bin/sh 的 ASCII 码
pushq %rdi # 入栈
movq %rsp, %rdi # 参数 1,命令行参数
xor %rsi, %rsi # 参数 2,环境变量
xor %rdx, %rdx # 参数 3,未使用
syscall # 调用系统调用
# 调用 exit() 函数退出程序
movq $60, %rax # 系统调用号为 60 表示 exit()
xor %rdi, %rdi # 返回值为 0
syscall # 调用系统调用
通过调用 execve() 函数执行 /bin/sh 命令,即打开一个新的 shell 终端。
# ps
PID TTY TIME CMD
17510 pts/0 00:00:00 bash
21407 pts/0 00:00:00 ps
# ./execve
# ps
PID TTY TIME CMD
17510 pts/0 00:00:00 bash
21426 pts/0 00:00:00 sh
21465 pts/0 00:00:00 ps
# exit
exit
# ps
PID TTY TIME CMD
17510 pts/0 00:00:00 bash
21550 pts/0 00:00:00 ps
execve:
# cat /usr/include/asm/unistd_64.h | grep execve
#define __NR_execve 59
NAME
execve - execute program
SYNOPSIS
#include <unistd.h>
int execve(const char *filename, char *const argv[],
char *const envp[]);
3.2 代码说明
(1)
xor %rax, %rax
这行指令将 rax 寄存器的值设置为 0,使用异或操作实现清零操作。
(2)
movb $59, %al
这行指令将系统调用号 59(即 execve() 函数)存储在 al 寄存器中。由于 al 寄存器是 rax 寄存器的低 8 位,因此可以使用 movb 指令将一个字节的值存储在 al 寄存器中。
(3)
xor %rdi, %rdi
这行指令将 rdi 寄存器的值设置为 0,表示参数 1(文件路径)为空。
(4)
movq $0x68732f6e69622f, %rdi
这行指令将 /bin/sh 的 ASCII 码存储在 rdi 寄存器中。具体来说,0x68732f6e69622f 是 /bin/sh 字符串的 ASCII 码的 64 位表示方式,由于 x86_64 架构是小端字节序,因此需要将高位字节存储在低地址。
这里给出两种获取字符串 ASCII 码的方法:
第一种方法:
/bin/sh 的 ASCII 码:
# echo -n "/bin/sh" | xxd -g 1
0000000: 2f 62 69 6e 2f 73 68
由于 x86_64 架构是小端字节序,因此需要将高位字节存储在低地址。
即:0x68732f6e69622f
第二种方法:
# python
>>> string = "/bin/sh"
>>> string[::-1].encode('hex')
'68732f6e69622f'
(5)
pushq %rdi
这行指令将 rdi 寄存器的值入栈,即将 /bin/sh 字符串的地址入栈。这是为了将字符串的地址传递给 execve() 函数。
(6)
movq %rsp, %rdi
这行指令将栈顶指针存储在 rdi 寄存器中,表示参数 2(命令行参数)。由于 /bin/sh 字符串的地址已经入栈,因此栈顶指针即为字符串的地址。
(7)
xor %rsi, %rsi
xor %rdx, %rdx
这两行指令分别将参数 3(环境变量)和参数 4(未使用)清零,使得 execve() 函数不需要传递这些参数。由于这两个参数都可以为空,因此可以使用异或操作清零。
(8)
syscall
这行指令调用 execve() 系统调用,执行 /bin/sh 命令。文章来源:https://www.toymoban.com/news/detail-532725.html
在 Linux x86_64 架构下:
执行系统调用时,系统调用号存储在 rax 寄存器中,参数 1~6 存储在 rdi、rsi、rdx、r10、r8 和 r9 寄存器中。
进行普通的函数调用时,rax 寄存器用于保存返回值,参数 1~6 存储在 rdi、rsi、rdx、rcx、r8 和 r9 寄存器中。文章来源地址https://www.toymoban.com/news/detail-532725.html
到了这里,关于Linux x86_64 汇编语言的编写的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!