汇编教程十五(x86汇编/Linux的系统接口int 0x80 & syscall)

  

前言: 

  系统调用是用户程序和 Linux 内核之间的接口。它们用于让内核执行各种系统任务,例如文件访问、进程管理和网络。在 C 编程语言中,您通常会调用包装函数来执行所有必需的步骤,甚至使用标准IO库等高级功能(用户空间不能直接访问内核系统调用)。
   在 Linux 上有几种方法可以进行系统调用。本博文将重点介绍通过使用或调用软件中断来进行系统调用。这是在纯汇编程序中进行系统调用的一种简单直观的方法。 例如: 32位 int $0x80  ,64位 syscall。
    本博文调研基于2.6.38.5 内核

实验环境:
     

[root@aozhejin /usr/local/src/vdso/qita]$cat /etc/redhat-release 
CentOS Linux release 7.9.2009 (Core)
[root@aozhejin /usr/local/src/vdso/qita]$uname -r
3.10.0-1160.62.1.el7.x86_64
//这里并没有使用3.10的内核分析,是为了比较差异

 

一、内核启动之系统调用初始化
  

      1).在内核启动早期初始化系统调用

       linux 内核启动入口调用链条.......

        init/main.c

   start_kernel
-------> trap_init # arch/x86/kernel/traps.c -----------> cpu_init # arch/x86/kernel/cpu/common.c ----------------> syscall_init # arch/x86/kernel/cpu/common.c

     下面是相关的内容

 1.Linux syscall interfaces (non-arch-specific) 
E:\linux内核\linux-2.6.38.5\include\linux\syscalls.h

 

定义系统调用,使用 SYSCALL_DEFINEn()
  #define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
  #define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
  #define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
  #define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
  #define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
  #define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
.....

 

2.E:\linux内核\linux-2.6.38.5\arch\x86\kernel\syscall_64.c

 

/* System call table for x86-64. 
我们通常查询时候,是 https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscall_64.c
sys_call_table 数组的大小为 __NR_syscall_max + 1 ,_NR_syscall_max 宏作为给定架构的系统调用最大数量。
*/ #include <linux/linkage.h> #include <linux/sys.h> #include <linux/cache.h> #include <asm/asm-offsets.h> #define __NO_STUBS #define __SYSCALL(nr, sym) extern asmlinkage void sym(void) ; #undef _ASM_X86_UNISTD_64_H #include <asm/unistd_64.h> #undef __SYSCALL #define __SYSCALL(nr, sym) [nr] = sym, #undef _ASM_X86_UNISTD_64_H typedef void (*sys_call_ptr_t)(void); extern void sys_ni_syscall(void); const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = { /* * Smells like a like a compiler bug -- it doesn't work when the & below is removed. */ [0 ... __NR_syscall_max] = &sys_ni_syscall, #include <asm/unistd_64.h> //指 arch\x86\include\asm\unistd_64.h };

 

3.E:\linux内核\linux-2.6.38.5\arch\x86\include\asm\unistd.h  (废弃)
4.E:\linux内核\linux-2.6.38.5\include\asm-generic\syscalls.h 补丁,system calls约定可能会被覆盖
5.E:\linux内核\linux-2.6.38.5\include\asm-generic\syscall.h
6.E:\linux内核\linux-2.6.38.5\arch\x86\include\asm\unistd_32.h

 

#ifndef _ASM_X86_UNISTD_32_H
#define _ASM_X86_UNISTD_32_H

/*
 *这个文件包含system call numbers ,一共341个系统调用+其它.
 */
#define __NR_restart_syscall      0
#define __NR_exit          1
#define __NR_fork          2
#define __NR_read          3
#define __NR_write          4
#define __NR_open          5
#define __NR_close          6
#define __NR_waitpid          7
#define __NR_creat          8
#define __NR_link          9
#define __NR_unlink         10
#define __NR_execve          11
#define __NR_chdir         12
#define __NR_time         13
#define __NR_mknod         14
#define __NR_chmod         15
#define __NR_lchown          16
#define __NR_break         17
...

 

7.E:\linux内核\linux-2.6.38.5\linux-2.6.38.5\arch\x86\include\asm\unistd_64.h  //系统调用号定义
详细使用: https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md

 

#ifndef _ASM_X86_UNISTD_64_H
   #define _ASM_X86_UNISTD_64_H
#ifndef __SYSCALL
   #define __SYSCALL(a, b)
#endif
/*
 * 这个文件包含了 system call numbers.
 */

/* at least 8 syscall per cacheline */
#define __NR_read    0   __SYSCALL(__NR_read, sys_read)
#define __NR_write   1   __SYSCALL(__NR_write, sys_write)
#define __NR_open    2  __SYSCALL(__NR_open, sys_open)
#define __NR_close   3   __SYSCALL(__NR_close, sys_close)
#define __NR_stat    4   __SYSCALL(__NR_stat, sys_newstat)
#define __NR_fstat   5   __SYSCALL(__NR_fstat, sys_newfstat)
#define __NR_lstat   6   __SYSCALL(__NR_lstat, sys_newlstat)
#define __NR_poll    7   __SYSCALL(__NR_poll, sys_poll)
....

 

  2).外部 调用入口

    E:\linux内核\linux-2.6.38.5\arch\x86\kernel\entry_64.S

1)对于 64 位代码,可以从 arch/x86/kernel/entry_64.S 的system_call程序入口点访问该表;
   它使用 RAX 寄存器来选择数组中的相关条目,然后调用它。
2)在函数的前面, SAVE_ARGS宏将各种寄存器压入堆栈,以匹配我们之前看到的 asmlinkage指令。
注意:这里仅能从用户空间来调用
代码如下:
/*
 * System call entry. Upto 6 arguments in registers are supported.
 *
 * SYSCALL does not save anything on the stack and does not change the
 * stack pointer.
 */

/*
  参看: https://www.cnblogs.com/aozhejin/p/17207212.html
* Register setup: * rax system call number * rdi arg0 * rcx return address for syscall/sysret, C arg3 * rsi arg1 * rdx arg2 * r10 arg3 (--> moved to rcx for C) * r8 arg4 * r9 arg5 * r11 eflags for syscall/sysret, temporary for C * r12-r15,rbp,rbx saved by C code, not touched. * * Interrupts are off on entry. * Only called from user space. * * XXX if we had a free scratch register we could save the RSP into the stack frame * and report it properly in ps. Unfortunately we haven't. * * When user can change the frames always force IRET. That is because * it deals with uncanonical addresses better. SYSRET has trouble * with them due to bugs in both AMD and Intel CPUs. */
ENTRY(system_call)
CFI_STARTPROC    simple
CFI_SIGNAL_FRAME
CFI_DEF_CFA     rsp,KERNEL_STACK_OFFSET
CFI_REGISTER    rip,rcx
.....
参看: https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md
 

 

二、32位(x86-64系统)进行系统调用

    要使用中断进行系统调用,您必须通过将所有必需信息复制到通用gpr寄存器(eax)中将它们传递给内核。
      同时会使用ebx, ecx, 和 edx等其它通用寄存器来进行系统调用的参数的存储. 这些寄存器中的信息按顺序存储,这意味着第一个参数保存在 中ebx,第二个保存在 中ecx,依此类推。
      general-purpose register 缩写为GPR(英文资料中会用到)

    每个系统调用都有一个固定的编号。Linux 始终保证向后兼容性,因此一旦将一个数字分配给系统调用,它就永远不会改变。 

    您通过将十六进制数字写入/寄存器来指定系统调用 eax/rax register.

 1、一个完整的32位汇编程序的调用样例,把字符串输出到屏幕(%和注解;就可以区分intel还是AT&T风格):
        section .data           ; data section
msg:    db "this is test",10     ; the string to print, 10=cr
len:    equ $-msg               ; "$" means "here"
                                ; len is a value, not an address

        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:                           ; standard  gcc  entry point
_start:
        mov    edx,len            ; arg3, length of string to print
        mov    ecx,msg            ; arg2, 输出内容的长度
        mov    ebx,1              ; arg1, 调用号
        mov    eax,4              ; write command to int 80 hex
        int    0x80               ; interrupt 80 hex, call kernel

        mov    ebx,0              ; exit code, 错误代码,设置为0标识正常
        mov    eax,1              ; exit command to kernel
        int    0x80               ; interrupt 80 hex, call kernel

  大多数系统调用都采用参数来执行其任务。这些参数是通过在进行实际调用之前将它们写入适当的寄存器来传递的。每个参数索引都有一个特定的寄存器。请参阅小节中的表格,因为和之间的映射不同参数按照它们在相应 C 包装函数的函数签名中出现的顺序传递。您可以在每个 Linux ABI 文档中找到系统调用函数及其签名

  大多数系统调用都采用参数来执行其任务.
       这些参数是通过在进行实际调用之前将它们写入适当的寄存器来传递的。每个参数索引都有一个特定的寄存器.
       请看看 int $0x80 和 syscall的不同之处.
  参数按照它们在相应C包装函数的函数签名中出现的顺序传递.                   
       你也可以找到系统调用函数和他们的签名在每个linux ABI文档中 
  linux 键入命令man 2 open 来查看 open 系统调用签名

 [root@ht6 asinstruction2]# man 2 open 

  一切设置正确后,您可以使用 int $0x80 或 syscall,让内核执行任务。

    return / error 系统调用的值会被写入到 eax/rax寄存器.

    内核使用自己的栈来完成这个事情. 用户栈不会以任何方式被触碰。

      2、我们继续分析内核源码
        trap_init中进行了下面的处理

//start_kernel --->trap_init(); 初始化
void
__init trap_init(void) { ... //如果是32位程序-->init 0x80 #ifdef CONFIG_IA32_EMULATION set_system_intr_gate(IA32_SYSCALL_VECTOR, ia32_syscall); set_bit(IA32_SYSCALL_VECTOR, used_vectors); #endif //如果64位程序--->syscall #ifdef CONFIG_X86_32 set_system_trap_gate(SYSCALL_VECTOR, &system_call); set_bit(SYSCALL_VECTOR, used_vectors); #endif ... }

    32位 int 0x80 系统调用(ia32_syscall)
      Linux 内核为32位架构程序预留了一个特殊的软中断号 128 (0x80), 用户空间程序使用它可以进入内核执行系统调用,对应的中断处理函数是 ia32_syscall。
     其中 IA32_SYSCALL_VECTOR 为 0x80,定义在 arch/x86/include/asm/irq_vectors.h(irq中断vector布局)


/*
* Linux IRQ vector layout.
* 这个文件中是irq 向量布局
* 这里 定义了256个 IDT 条目 (每个cpu - 每个条目是8个字节)

 They are used as a jump table by the CPU when a given vector is triggered - by a CPU-external, CPU-internal or
* software-triggered event.
* Linux sets the kernel code address each entry jumps to early during
* bootup, and never changes them. This is the general layout of the
* IDT entries:
* Vectors 0 ... 31 : system traps and exceptions - hardcoded events
* Vectors 32 ... 127 : device interrupts
* Vector 128 :   传统的 int80 syscall 接口(int 0x80)
* Vectors 129 ... 237 : device interrupts
* Vectors 238 ... 255 : special interrupts
*
* 64-bit x86 has per CPU IDT tables, 32-bit has one shared IDT table.
*
* This file enumerates the exact layout of them:
*/


#define
IA32_SYSCALL_VECTOR 0x80 #ifdef CONFIG_X86_32 //如果定义了这个宏 # define SYSCALL_VECTOR 0x80 //则定义SYSCALL_VECTOR #endif

   中断触发在

...
/*
Compatibility mode system call entry point for x86-64. 兼容现代系统调用入口指针,用于x86-64 本程序为32位的系统调用 */
/* 
 * 仿真 IA32(32位架构) system calls 通过 int 0x80 (指令). 
 *
 * 参数:     
 * %eax System call number.
 * %ebx Arg1
 * %ecx  Arg2
 * %edx  Arg3
 * %esi   Arg4
 * %edi   Arg5
 * %ebp  Arg6    [注意: 它不会保存在 栈帧上, 不应该碰它意思是不设置它]
 *
 * Notes:
 * Uses the same stack frame as the x86-64 version.    
 * All registers except %eax must be saved (but ptrace may violate that)
 * Arguments are zero extended. For system calls that want sign extension and
 * take long arguments a wrapper is needed. Most calls can just be called
 * directly.
 * Assumes it is only called from user space and entered with interrupts off.    
 */ 

ENTRY(ia32_syscall)
    CFI_STARTPROC32    simple
    CFI_SIGNAL_FRAME
    CFI_DEF_CFA    rsp,SS+8-RIP
    /*CFI_REL_OFFSET    ss,SS-RIP*/
    CFI_REL_OFFSET    rsp,RSP-RIP
    /*CFI_REL_OFFSET    rflags,EFLAGS-RIP*/
    /*CFI_REL_OFFSET    cs,CS-RIP*/
    CFI_REL_OFFSET    rip,RIP-RIP
    PARAVIRT_ADJUST_EXCEPTION_FRAME
    SWAPGS
    /*
     * No need to follow this irqs on/off section: the syscall
     * disabled irqs and here we enable it straight after entry:
     */
    ENABLE_INTERRUPTS(CLBR_NONE)
    movl %eax,%eax
    pushq %rax
    CFI_ADJUST_CFA_OFFSET 8
    cld
    /* note the registers are not zero extended to the sf.
       this could be a problem. */
    SAVE_ARGS 0,0,1
    GET_THREAD_INFO(%r10)
    orl   $TS_COMPAT,TI_status(%r10)
    testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%r10)
    jnz ia32_tracesys
    cmpq $(IA32_NR_syscalls-1),%rax
    ja ia32_badsys
ia32_do_call:
    IA32_ARG_FIXUP
    call *ia32_sys_call_table(,%rax,8) # xxx: rip relative
ia32_sysret:
    movq %rax,RAX-ARGOFFSET(%rsp)
ia32_ret_from_sys_call:
    CLEAR_RREGS -ARGOFFSET
    jmp int_ret_from_sys_call 

ia32_tracesys:             
    SAVE_REST
    CLEAR_RREGS
    movq $-ENOSYS,RAX(%rsp)    /* ptrace can change this for a bad syscall */
    movq %rsp,%rdi        /* &pt_regs -> arg1 */
    call syscall_trace_enter
    LOAD_ARGS32 ARGOFFSET  /* reload args from stack in case ptrace changed it */
    RESTORE_REST
    cmpq $(IA32_NR_syscalls-1),%rax
    ja  int_ret_from_sys_call    /* ia32_tracesys has set RAX(%rsp) */
    jmp ia32_do_call
END(ia32_syscall)
https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_node/ld_24.html#SEC24

 

 

三、32位和64位的系统调用

    1)x86-32(IA32即32位体系)

   在Linux x86和Linux x86_64系统上,可以通过调用 $0x80 中断来进行系统调用 , 使用  int 指令(int $0x80).
        通过如下设置通用寄存器(gpr)来传递参数:

使用寄存器映射进行系统调用 int $0x80
系统调用号第一个参数第二个参数第三个参数第四个参数第五个参数第六个参数返回结果
eax ebx ecx edx esi edi ebp eax


     linux 系统调用号描述在 linux 的文件中(我的系统是linux centos7):

  [root@ht6 asinstruction2]# ls /usr/include/asm/unistd_32.h

在使用 int $0x80 系统调用期间保留所有寄存器.

2)x86-64(64位体系)

x86_64即64位体系结构引入了一个专用指令来进行系统调用。它不访问中断描述符表,速度更快。通过如下设置GPR来传递参数:

使用寄存器映射进行系统调用 syscall
系统调用号第一个参数第二个参数 第三个参数 第四个 参数 第五个 参数 第六个 参数 result
rax rdi rsi rdx r10 r8 r9 rax


linux 系统调用号描述在 linux 的文件中(我的系统是linux centos7):

[root@ht6 asinstruction2]# cat /usr/include/asm-generic/unistd.h

[root@ht6 asinstruction2]# cat /usr/include/asm/unistd_64.h

 
    在用syscall 进行系统调用期间,所有的寄存器, 除了 rcx and r11 (and the return value, rax)寄存器, 会被保留

   如何执行系统调用取决于您的操作系统和体系结构。对于Linux:

                   该表来自:  Chromium OS syscall pages

ArchitectureSyscall
instruction
Syscall
number in
return
value
arg0arg1arg2arg3arg4arg5
x86_64 syscall rax rax rdi rsi rdx r10 r8 r9
x86 int 0x80 eax eax ebx ecx edx esi edi ebp
arm svc 0 r7 r0 r0 r1 r2 r3 r4 r5
arm64 svc 0 x8 x0 x0 x1 x2 x3 x4 x5


注意:

为了实现最大的兼容性,在 64 位平台上,Linux 操作系统使用系统调用输入和输出 的方法。这意味着,例如,您不能使用 int $0x80 在 x86-64 平台上传递或接收(完整的)64 位地址指针,因为所有参数的高 32 位和结果都为零。这通常使用syscall来完成对齐,因为它比中断更快。 
       


三、linux c库调用

在x86-64 Linux的C库函数的调用中,参数6被传递到r9上,其他参数被传递到栈上(按相反的顺序)。

register mapping for library call
第一个参数第二个 参数第三个 参数第四个 参数第五个 参数第六个 参数
rdi rsi rdx rcx r8 r9


   调用者(caller)可以期望在寄存器 rax 中找到子例程(例程一般都是在汇编当中定义的概念)的返回值

范例

为了总结和澄清信息,让我们看一个非常简单的示例:   hello world程序
它会使用write 系统调用写入文本 "hello world" 到标准输入输出 并且 使用 _exit 系统调用来退出程序  

Syscall 签名:

ssize_t write(intfd,constvoid*buf,size_tcount);
void   _exit(intstatus);

这是C程序,在下面的汇编中实现:

#include<unistd.h>
int main(intargc,char*argv[])
{
    write(1,"Hello World\n",12);/* write "Hello World" to stdout */
    _exit(0);/* exit with error code 0 (no error) */
}

两个示例的开头相同: 一个字符串存储在 data segment 并且  _start 作为全局的入口.

  .global _start
.text
_start:
  ..... msg:
  .ascii "Hello World\n"

int $0x80(32位程序)

   定义在 $build/usr/include/asm/unistd_32.h,  write and _exit 系统调用号是:

#define __NR_exit 1
#define __NR_write 4

参数的传递与C程序中的传递完全相同, 使用了恰当的的寄存器. 设置好所有内容后,系统调用将使用int $0x80
下面是32位程序范例:

_start:
    movl $4,%eax     ; use the `write` [interrupt-flavor] system call
    movl $1,%ebx     ; write to stdout
    movl $msg,%ecx   ; use string "Hello World"
    movl $12,%edx    ; write 12 characters
    int  $0x80        ; make system call

    movl $1,%eax     ; use the `_exit` [interrupt-flavor] system call
    movl $0,%ebx     ; error code 0
    int  $0x80       ; make system call

syscall (64汇编程序)

    运行期定义在  usr/include/asm/unistd_64.h
    内核代码定义在 E:\linux内核\linux-2.6.38.5\arch\x86\include\asm\unistd_64.h
  syscall  系统调用号定义为:

#define __NR_write 1
#define __NR_exit 60

 参数的传递与int $0x80 中的样例一样,
 除了寄存器的顺序不同.  使用 syscall 指令来进行系统调用

_start:
     movq $1,%rax   ; use the `write` [fast] syscall
     movq $1,%rdi   ; write to stdout
     movq $msg,%rsi ; use string "Hello World"
     movq $12,%rdx  ; write 12 characters
     syscall       ; make syscall

     movq $60,%rax  ; use the `_exit` [fast] syscall
     movq $0,%rdi   ; error code 0
     syscall     ; make syscall

c库调用

     这是一个示例库函数的C原型。

  WindowXCreateWindow(display,parent,x,y,width,height,border_width,depth,class,visual,valuemask,attributes)

  Parameters are passed just like in the int $0x80 example, except that the order of the registers is different.

Library function is declared at the beginning of the source file (and the path to the library, at compilation-linking time).

externXCreateWindow
mov rdi,[xserver_pdisplay]
mov rsi,[xwin_parent]
mov rdx,[xwin_x]
mov rcx,[xwin_y]
mov r8,[xwin_width]
mov r9,[xwin_height]
mov rax,attributes
push rax; ARG 12
sub rax,rax
mov eax,[xwin_valuemask]
push rax; ARG 11
mov rax,[xwin_visual]
push rax; ARG 10
mov rax,[xwin_class]
push rax; ARG 9
mov rax,[xwin_depth]
push rax; ARG 8
mov rax,[xwin_border_width]
push rax; ARG 7
call XCreateWindow
mov [xwin_window],rax

     请注意,推送到栈中的函数的最后一个参数是以相反的顺序完成的。 ect to find the return value of the subroutine in the register rax.

 参考:
    http://comet.lehman.cuny.edu/jung/cmp426697/LinuxSystemCalls.pdf
    https://en.wikibooks.org/wiki/X86_Assembly/Interfacing_with_Linux
   
https://www.cs.uaf.edu/2017/fall/cs301/lecture/11_17_syscall.html
   https://0xax.gitbooks.io/linux-insides/content/Misc/linux-misc-4.html
  http://flint.cs.yale.edu/cs422/doc/ELF_Format.pdf
   https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md   (linux syscall 表)
  https://arthurchiao.art/blog/system-call-definitive-guide-zh/
  https://www.educative.io/answers/what-are-system-calls-in-assembly-language
 https://blog.packagecloud.io/the-definitive-guide-to-linux-system-calls/
https://man7.org/linux/man-pages/man2/syscalls.2.html   系统调用表

posted @ 2023-03-12 09:28  jinzi  阅读(1)  评论(0)    收藏  举报