汇编教程十三(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)

  [root@ht6 asm]# as -v 
  GNU assembler version 2.27 (x86_64-redhat-linux) using BFD version version 2.27-44.base.el7_9.1

  [root@ht6 asinstruction]# ld -v
   GNU ld version 2.27-44.base.el7_9.1

  [root@ht6 asinstruction]# nasm -v
   NASM version 2.10.07 compiled on Jun 9 2014

 
一、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) 可以对照着看系统调用号和系统调用函数的对应关系.

使用寄存器映射进行系统调用 int $0x80
系统调用号system call numberarg0arg1arg2arg3arg4arg5返回结果
  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

posted @ 2023-03-11 14:50  jinzi  阅读(542)  评论(0编辑  收藏  举报