汇编教程十三(64位汇编程序和32位汇编程序的汇编系统调用实验)
前言:
“什么是系统调用?” 如果你曾经写过 DOS 汇编程序(大多数 IA-32 汇编程序员都写过),你可能还记得 DOS 服务 int 0x21、 int 0x25、 int 0x26 等。这些类似于 UNIX 系统调用。然而,实际的实现是完全不同的,系统调用不一定是通过某种中断来完成的。此外,DOS 程序员经常将操作系统服务与 BIOS 服务(如 int 0x10 或 int 0x16)混合使用 ,并且当他们无法在 UNIX 中执行它们时感到非常惊讶,因为这些不是操作系统服务)。
在 UNIX 操作系统中执行系统调用有两种常见的方法:通过 C 库 ( libc ) 包装器,或直接。
本文将展示如何使用直接内核调用,因为这是调用内核服务的最快方式;我们的代码没有链接到任何库,不使用 ELF 解释器,它直接与内核通信。
Linux 中的系统调用是通过 int 0x80 中断完成的,Linux 不同于通常的 UNIX 调用约定,它具有用于系统调用的“fastcall”约定(它类似于 DOS)。系统调用号在eax寄存中传递 ,参数通过其他寄存器传递,而不是栈。因此,在ebx、 ecx、 edx、 esi、 edi、 ebp等寄存器中最多可以有六个参数 。如果有更多参数,它们只是作为第一个参数通过结构传递。结果会在 eax寄存器中返回,栈根本没有被触及到。
实验环境:
注意本环境已安装32位glibc兼容库,执行下面的操作即可
[root@ht6 test]# yum -y install glibc-devel.i686 glibc-devel ibstdc++-devel.i686
系统调用调用号在 /usr/include/sys/syscall.h中,但实际上在 /user/includes/asm/unistd.h中
[root@ht6 asinstruction2]# uname -a #内核版本
Linux ht6.node 3.10.0-1160.62.1.el7.x86_64 #1 SMP Tue Apr 5 16:57:59 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
[root@ht6 asinstruction2]# ll /usr/include/sys/sys* -rw-r--r-- 1 root root 1348 May 18 2022 /usr/include/sys/syscall.h -rw-r--r-- 1 root root 2023 May 18 2022 /usr/include/sys/sysctl.h -rw-r--r-- 1 root root 1524 May 18 2022 /usr/include/sys/sysinfo.h -rw-r--r-- 1 root root 7701 May 18 2022 /usr/include/sys/syslog.h -rw-r--r-- 1 root root 2553 May 18 2022 /usr/include/sys/sysmacros.h [root@ht6 asinstruction2]# ll /usr/include/asm/unistd* -rw-r--r-- 1 root root 9593 Apr 6 2022 /usr/include/asm/unistd_32.h -rw-r--r-- 1 root root 8793 Apr 6 2022 /usr/include/asm/unistd_64.h -rw-r--r-- 1 root root 296 Apr 6 2022 /usr/include/asm/unistd.h -rw-r--r-- 1 root root 15478 Apr 6 2022 /usr/include/asm/unistd_x32.h
本环境兼容32位程序的编译,采用的gcc编译环境,gdb作为调试工具。同时安装有nasm
[root@ht6 test]# uname -a #内核版本 Linux ht6.node 3.10.0-1160.62.1.el7.x86_64 #1 SMP Tue Apr 5 16:57:59 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux [root@ht6 test]# cat /etc/redhat-release #操作系统 CentOS Linux release 7.9.2009 (Core) [root@ht6 test]# lscpu Architecture: x86_64 #cpu位数-64位 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian #网络字节序 CPU(s): 8 #cpu核数 On-line CPU(s) list: 0-7 Thread(s) per core: 1 Core(s) per socket: 4 #有4个插槽 Socket(s): 2 NUMA node(s): 1 #具体请看我的另一篇博文 Vendor ID: GenuineIntel CPU family: 6 #处理器系列 Model: 63 Model name: Intel(R) Xeon(R) CPU E5-2660 v3 @ 2.60GHz Stepping: 2 CPU MHz: 2593.993 BogoMIPS: 5187.98 Hypervisor vendor: VMware Virtualization type: full L1d cache: 32K #cpu内部集成的一级缓存(数据) L1i cache: 32K #cpu内部集成的一级缓存(指令) L2 cache: 256K #cpu内部集成的二级缓存(不分数据和指令) L3 cache: 25600K NUMA node0 CPU(s): 0-7 [root@ht6 test]# getconf LONG_BIT 64 //64位 [root@ht6 test]# arch x86_64
[root@ht6 asm]# gcc -v #版本很重要,决定了字节对接等
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/lto-wrapper
Target: x86_64-redhat-linux
Configured with: ../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info
Thread model: posix
gcc version 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)
一、64位汇编程序测试
以下为使用Linux 调用系统中断的64位汇编程序 -AT&T风格
如果不清楚在纯汇编程序中进行系统调用 int $0x80 可参考 这里
# ----------------------------------------------------------------------------- # 一个 64-bit Linux 独立的系统调用程序仅仅是使用系统调用,输出到控制台 # 程序不需要链接到任何其他库 # # https://www.cnblogs.com/aozhejin/ # # 系统调用: # 1: write(fileid, bufferAddress, numberOfBytes) # 60: exit(returnCode) # # 编译: # gcc -c 64bit.s # 链接: # ld 64bit.o (生成 a.out) # 或者 # ld -o 64bit 64bit.o (生成 64bit) # # 或者你直接编译和链接: # gcc -nostdlib -o 64bit 64bit.s (生成64bit二进制程序) # # 字符 _start 是默认的入口对于ld来说. # ----------------------------------------------------------------------------- .global _start .text _start: #entry point(linker(ld) needs this) # write(1, msg, 14) mov $1, %rax # 写入0x1 到rax寄存器,系统调用号1 (sys_write系统调用号=0x1)
# sys_write是系统调用函数,对磁盘文件进行写操作 mov $1, %rdi # 写入文本1到rai寄存器 , 输出到控制台 mov $msg, %rsi # 写入msg变量内容到rsi mov $14, %rdx # 你需要计算msg变量中的内容的字节数,在这里设置14代表14字节 syscall # 调用系统call(sys_write) # exit(0) mov $60, %rax # 系统调用 60 退出 xor %rdi, 0 # we want return code 0 syscall # invoke operating system to exit msg: .ascii "这是一个 64 bit 程序 !\n"
生成 64bit 可执行文件并执行
[root@ht6 asinstruction]# gcc -g -c 64bit.s && ld -g -o 64bit 64bit.o && ./64bit
this is 64bit
或
[root@ht6 asinstruction]# gcc -nostdlib -o 64bit 64bit.s && ./64bit #不使用glibc库
this is 64bit
gdb调试:
1)执行第一步
[root@ht6 asinstruction]# gdb -q -tui ./64bit
(gdb)b _start
(gdb)r
(gdb)layout regs
下图是代码和寄存器布局图(-g 加之后才能看到代码调试视图)
_start 的地址为 0x400078 (程序刚执行时的入口地址,这个地址被放入了eip寄存器中)
2)执行第二步
[root@ht6 asinstruction]# gdb -q -tui ./64bit
(gdb)b _start
(gdb)r
(gdb)layout regs
(gdb)si #si即执行下步汇编指令
这步将执行 mov $1, %rax
解释一下,当执行这条指令时,我们看到如下变化: rax寄存器 写入1 即 0x1(不是十进制的1,是十六进制的0x1) rip 指令指针寄存器,里面写入了十六进制内存地址 0x400078 (_start+7) 注: _start 起始地址即 0x400078 |
我们结合着反汇编结果看:
[root@ht6 asinstruction]# objdump -d 64bit 64bit: file format elf64-x86-64 Disassembly of section .text: 0000000000400078 <_start>: 400078: 48 c7 c0 01 00 00 00 mov $0x1,%rax //第一条指令,eip地址为0x400078 40007f: 48 c7 c7 01 00 00 00 mov $0x1,%rdi //第二条指令, eip地址为_start+7(0x400078+7) 400086: 48 c7 c6 a2 00 40 00 mov $0x4000a2,%rsi //第三条指令, eip地址为0x400086
40008d: 48 c7 c2 0e 00 00 00 mov $0xe,%rdx 400094: 0f 05 syscall 400096: 48 c7 c0 3c 00 00 00 mov $0x3c,%rax 40009d: 48 31 ff xor %rdi,%rdi 4000a0: 0f 05 syscall //400078即内存地址 0x400078, 红色部分即操作码(指令解析器根据指令编码规则来生成指令) , mov $0x1,$rax 即指令 00000000004000a2 <message>: 4000a2: 74 68 je 40010c <message+0x6a> 4000a4: 69 73 20 69 73 20 36 imul $0x36207369,0x20(%rbx),%esi 4000ab: 34 62 xor $0x62,%al 4000ad: 69 .byte 0x69 4000ae: 74 20 je 4000d0 <message+0x2e> 4000b0: 0a .byte 0xa
执行截图
2)执行第三步
2)执行第二步 [root@ht6 asinstruction]# gdb -q -tui ./64bit (gdb)b _start (gdb)r (gdb)layout regs (gdb)si #si即执行第二步汇编指令 (gdb)si #si即执行第三步汇编指令
(gdb)disas #查看eip寄存器内存放的内存地址步进情况
Dump of assembler code for function _start:
=> 0x0000000000400078 <+0>: mov $0x1,%rax
0x000000000040007f <+7>: mov $0x1,%rdi
0x0000000000400086 <+14>: mov $0x4000a2,%rsi
0x000000000040008d <+21>: mov $0xe,%rdx
0x0000000000400094 <+28>: syscall
0x0000000000400096 <+30>: mov $0x3c,%rax
0x000000000040009d <+37>: xor %rdi,%rdi
0x00000000004000a0 <+40>: syscall
End of assembler dump.
执行 mov $1, %rdi
mov $1,%rdi 表示把0x1写入rdi寄存器 rip 寄存器显示 0x40007f (_start+14) ,这里的14要说一下: 注:_start+14 表示 0x400078+E(二进制14)=0x400086 之前没有介绍_start+7 因为十六进的7和十进制的7相等,所以看不出来这个数字上的区别,但是14就不同,14是对应的是十六进制的E,所以一下子能看出来区别的 1)_start的内存地址为: 0x400078 2)14是二进制表示,对应十六进制是E |
下面是执行时截图
.....
二、64位intel风格写法
64位程序Intel风格语法
segment .data msg : db "this is 64bit intel",10 ; global _start segment .text _start: ;入口(linker) mov rax,1 mov rdi,1 mov rsi,msg ;消息写入 mov rdx,20 ; 本行是必须,否则不会输出任何内容,20个字节足以输出msg变量的内容 syscall
#退出代码 mov rax,60 mov rdi,0 syscall #保存为 intel64.asm 执行:
生成64位可执行程序并执行
[root@ht6 asinstruction]# nasm -felf64 intel64.asm && ld -o intel64 intel64.o && ./intel64
this is 64-bit intel
[root@ht6 asinstruction]# file intel64 intel64: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
gdb测试
[root@ht6 asinstruction]# gdb -q -tui intel64 child process 115535 In: _start Line: ?? PC: 0x4000b0 Reading symbols from /usr/local/src/asinstruction/intel64...done. (gdb) b _start Breakpoint 1 at 0x4000b0 (gdb) r Starting program: /usr/local/src/asinstruction/intel64 Breakpoint 1, 0x00000000004000b0 in _start () (gdb) list #把代码列出来,每次跳动18行,所以输入一次就够了 (gdb)disas #查看eip情况,注意 + 多少都是按照_start的内存起始地址开始计算的。 Dump of assembler code for function _start: 0x00000000004000b0 <+0>: mov $0x1,%eax 0x00000000004000b5 <+5>: mov $0x1,%edi #_start+5 0x00000000004000ba <+10>: movabs $0x6000d8,%rsi #_start+10 => 0x00000000004000c4 <+20>: mov $0x14,%edx 0x00000000004000c9 <+25>: syscall 0x00000000004000cb <+27>: mov $0x3c,%eax 0x00000000004000d0 <+32>: mov $0x0,%edi 0x00000000004000d5 <+37>: syscall End of assembler dump. (gdb) info line 11 #查看代码第11行的指令所在的内存地址初始地址 Line 11 of "intel64.asm" is at address 0x4000c4 <_start+20> but contains no code. (gdb) disassemble /r 0x00000000004000c4,0x00000000004000c9 Dump of assembler code from 0x4000c4 to 0x4000c9: => 0x00000000004000c4 <_start+20>: ba 14 00 00 00 mov $0x14,%edx End of assembler dump.
执行情况看下截图
三、32位汇编程序测试
intel风格语法,采用nasm进行测试
首先x86-32 的系统调用设置和使用寄存器规则如下: 具体请看 汇编教程十五(x86汇编/Linux的接口syscall)
详细具体请看: linux system call table (x86-32) 可以对照着看系统调用号和系统调用函数的对应关系.
系统调用号 | system call number | arg0 | arg1 | arg2 | arg3 | arg4 | arg5 | 返回结果 |
---|---|---|---|---|---|---|---|---|
eax |
ebx |
ecx |
edx |
esi |
edi |
ebp |
eax |
|
4 | 0x04 |
unsigned int fd |
const char *buf |
size_t count |
这里,arg0代表第一个参数,arg1代表第二个参数,以此类推。系统调用号在下面的内核源码分析中有阐述
汇编源码: intel32.s / intel64.s
; 演示一个基本的输出到屏幕,采用的是系统调用方式(int 0x80 有特殊意义) ; 32-bit(x86-32)------ ; 编译命令: nasm -g -f elf32 -l intel32.lst intel32.asm ; 链接命令: ld -g -m elf_i386 -o intel32 intel32.o ; 或: gcc -o intel32 intel32.o (only on 32-bit installations) ; ; 64-bit (x86-64/amd64) --- ; 编译: nasm -g -f elf64 -l intel64.lst intel64.asm ; 链接: ld -g -m elf_x86_64 -o intel64 intel64.o ; 或: gcc -o intel64 intel64.o (only on 64-bit installations) ; section .data ; data section msg: db "this is test",10 ; the string to print, 10=cr len: equ $-msg ; "$" means "here" ; len是一个值,不是一个地址,注意
section .text ; code section global main ; make label available to linker as invoked by gcc global _start ; make label available to linker w/ default entry main: ; 标准的 gcc 入口点 _start: mov edx,len ; 第三个参数, 消息长度 mov ecx,msg ; 第二个参数, 消息内容 mov ebx,1 ; 第一个参数, fd的描述符 mov eax,4 ; 写入系统调用号4,会调用系统调用函数 sys_writeint 0x80 ; interrupt 80 hex, call kernel ;汇编调用 linux系统调用的三行代码 mov ebx,0 ; exit code, 0=normal mov eax,1 ; exit command to kernel int 0x80 ; interrupt 80 hex, call kernel
来执行一下:
[root@ht6 asinstruction2]# nasm -g -f elf32 -l intel32.lst intel32.asm \\
&& ld -g -m elf_i386 -o intel32 intel32.o && ./intel32
结束输出: this is test
下面我把相关的代码执行次序修改一下,看看执行的情况, 下面红色部分为篡改位置的部分.
源代码为 intel32-2.s
section .text ; code section global main ; 告诉linker 被gcc调用global _start ; 告诉 linker w/ 默认入口点(必须为衔接器声明) main: ; 标准的gcc入口点 _start: mov eax,4 ;系统调用号(sys_write)
mov ebx,1 ;参数1, 文件描述符(标准输出) mov ecx,msg ;参数2, 要写入的消息 mov edx,len ;参数3, 要输出的消息的长度 int 0x80 ; 中断 0x80 , 调用内核 ;退出代码 mov ebx,0 ; exit code, 0=normal mov eax,1 ; exit command to kernel int 0x80 ; interrupt 80 hex, call kernel section .data ; data section msg: db "this is test",0xa ; 要打印的字符串, 10=cr len: equ $-msg ; "$" means "here" ; len is a value, not an address
执行之后,没有问题.
[root@ht6 asinstruction2]# nasm -g -f elf32 -l intel32-2.lst intel32-2.asm && ld -g -m elf_i386 -o intel32-2 intel32-2.o && ./intel32-2 this is test
四、内核源码角度分析下:
内核2.10版本源码参考:
E:\linux内核\linux-2.1.0\linux\arch\i386\kernel\entry.S E:\linux内核\linux-2.1.0\linux\include\asm-i386\unistd.h E:\linux内核\linux-2.1.0\linux\include\linux\sys.h
举例:
E:\linux内核\linux-2.1.0\linux\fs\read_write.c 摘取内容如下:
asmlinkage long sys_write(unsigned int fd, const char * buf, unsigned long count){ /* asmlinkage 是linux内核特殊针对c语言的用法,告诉编译程序,这里是采用通用寄存器来传递参数
asmlinkage使用的地方通常都是系统调用的函数. */ ... }
参看系统调用号和linux内核系统调用函数对应表,https://faculty.nps.edu/cseagle/assembly/sys_call.html
内核2.6.38.5版本源码参考:
E:\linux内核\linux-2.6.38.5\linux-2.6.38.5\arch\x86\ia32\ia32entry.S
E:\linux内核\linux-2.6.38.5\linux-2.6.38.5\include\asm-generic\syscall.h
//unistd.h此文件包含基于x86-64架构布局的所有系统调用号
E:\linux内核\linux-2.6.38.5\linux-2.6.38.5\include\asm-generic\unistd.h
centos7操作系统,内核3.10... 系统调用号被放置在
[root@ht6 asinstruction2]# cat /usr/include/asm/unistd_x32.h
[root@ht6 asinstruction2]# cat /usr/include/asm/unistd_32.h
#ifndef _ASM_X86_UNISTD_X32_H
#define _ASM_X86_UNISTD_X32_H 1
#注意这里是 0/1/2... 这些是十六进制数,如果在汇编中
#define __NR_read (__X32_SYSCALL_BIT + 0)
#define __NR_write (__X32_SYSCALL_BIT + 1)
#define __NR_open (__X32_SYSCALL_BIT + 2)
#define __NR_close (__X32_SYSCALL_BIT + 3)
#define __NR_stat (__X32_SYSCALL_BIT + 4)
#define __NR_fstat (__X32_SYSCALL_BIT + 5)
#define __NR_lstat (__X32_SYSCALL_BIT + 6)
#define __NR_poll (__X32_SYSCALL_BIT + 7)
#define __NR_lseek (__X32_SYSCALL_BIT + 8)
....... 省略
assembly language linux system call
以下为一次gdb调试错误信息:
1、用gdb的si命令 单步调试,从而 一步一步调试到某个指令错误上来方法,演示效果如下:
//由于编译时打开了 -g 所以代码模式可以看到每一步的调试 [root@ht6 asinstruction]# gdb -q -tui 32bit (gdb)b _start #断点开始
(gdb) r #运行 (gdb) si #单步调试,下一个指令 (gdb) si #汇编指令单步调试,下一个指令 (gdb) si #下一个指令 (gdb) si #下一个指令 (gdb) si #下一个指令 //当执行到第9行报错,说明该指令有问题 Program received signal SIGILL, Illegal instruction. _start () at 32bit.s:9
参考:
https://stackoverflow.com/questions/64415910/shellcode-illegal-instruction
https://cs.lmu.edu/~ray/notes/gasexamples/
https://montcs.bloomu.edu/Information/LowLevel/Assembly/linux-assembly-tutorial.shtml
https://www.baeldung.com/linux/compile-32-bit-binary-on-64-bit-os
https://flint.cs.yale.edu/cs421/papers/x86-asm/asm.html
https://en.wikipedia.org/wiki/X32_ABI
https://en.wikibooks.org/wiki/X86_Assembly/Interfacing_with_Linux (汇编x86,系统调用)
https://www.cnblogs.com/aozhejin/p/17207212.html 汇编和syscall、int 0x80
https://www.tutorialspoint.com/assembly_programming/assembly_system_calls.htm 系统调用表
旧的 linux 系统调用对应表
https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md#x86-32_bit
最新 linux 系统调用对应表
https://chromium.googlesource.com/chromiumos/docs/+/HEAD/constants/syscalls.md
https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_64.tbl
内核早期版本,系统调用很不错
https://asm.sourceforge.net/syscall.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)