linux内核系统调用之参数传递

电子说

1.3w人已加入

描述

与普通函数一样,系统调用通常需要一些输入/输出参数,这些参数可能包括实际值(即数字)、用户模式进程地址空间中的变量地址,甚至包括指向用户模式函数指针的数据结构的地址(参见第11章“信号相关的系统调用”部分)。

Linux系统下,所有系统调用的入口点都是system_call()或sysenter_entry(),这两个函数至少需要一个参数:系统调用号,其保存在寄存器eax中。要不然,内核怎么知道你想干什么呢?比如,我们在写多进程应用的时候,必须要用的fork()系统调用,在调用int $80或者sysenter汇编指令之前,必须设置eax寄存器的值为2(__NR_fork)。通常,这个操作都是在libc库中的函数中完成的,对于系统调用号我们不太在意而已。

fork()系统调用不需要其他参数。然而,许多系统调用确实需要额外的参数,这些参数必须由应用程序显式传递。例如,mmap()系统调用可能需要最多六个附加参数(除了系统调用号之外)。

普通C函数的参数通常将其值写入程序栈(用户态栈和内核态栈)来传递。因为系统调用是一种跨内核态和用户态的特殊函数,所以,用户态或内核态栈都不能使用。相反,在发起系统调用之前,将参数写入CPU寄存器中。然后内核在调用系统调用服务例程之前将存储在CPU寄存器中的参数复制到内核态栈上,因为后者是普通的C函数。

为什么内核不直接从用户栈复制参数到内核栈?首先,同时处理两个堆栈是很复杂的;其次,寄存器的使用使得系统调用处理程序的结构类似于其他异常处理程序的结构。

然而,要在寄存器中传递参数,必须满足2个条件:

每个参数的长度不能超过寄存器的长度(32位)。

除了在eax中传递的系统调用号之外,参数的数量不能超过6个,因为80×86处理器的寄存器数量非常有限。

第一个条件总是为真,因为根据POSIX标准,不能存储在32位寄存器中的大参数必须通过引用传递。一个典型的例子是settimeofday()系统调用,它必须读取64位结构。

对于需要6个以上参数的系统调用,使用单个寄存器指向进程地址空间中包含参数值的内存区域。当然,程序员不必关心其中的细节。与每个C函数调用一样,当调用封装服务例程时,参数会自动保存在栈上。这个服务例程会采用合适方法将参数传递给内核。

传递系统调用号及其参数的寄存器依次为:eax(系统调用号)、ebx、ecx、edx、esi、edi和ebp。如前所述,system_call()和sysenter_entry()通过使用SAVE_ALL宏将这些寄存器的值保存在内核栈上。因此,当系统调用服务例程进入栈时,它会找到system_call()或sysenter_entry()的返回地址,然后是存储在ebx中的参数(系统调用的第一个参数),存储在ecx中的参数,等等(参见第4章“为中断处理程序保存寄存器”一节)。这个堆栈配置与普通函数调用完全相同。因此,服务例程可以通过使用常用的c语言结构轻松地引用其参数。

让我们看一个示例。sys_write()服务例程,用来处理write()系统调用,声明如下:

 

int sys_write (unsigned int fd, const char * buf, unsigned int count)

 

C编译器会将其编译为汇编语言函数,并期望在栈顶,返回地址的正下方,也就是保存ebx,ecx和edx寄存器的地方找到fd、buf和count参数。

在许多情况下,系统调用可能不使用参数,但服务例程需要知道在系统调用发起之前CPU寄存器的内容。例如,do_fork()函数需要知道这些寄存器的值,以便将它们拷贝到子进程的thread字段域内(可以查看第3章的thread字段)。这种情况下,使用类型为pt_regs的单参数,允许服务例程访问内核栈保存的值(使用SAVE_ALL宏保存,查看第4章的do_IRQ函数)。

 

int sys_fork (struct pt_regs regs)

 

服务例程的返回值写入到eax寄存器中,这是C编译器自动完成的,当遇到return n;时就会自动将n保存到eax寄存器中。

1 验证参数

内核在响应用户系统调用之前必须检查所有系统调用参数。检查的类型取决于系统调用和具体参数。还是以write()系统调用为例:fd应该是一个标识文件的文件描述符,所以sys_write()必须检查fd是之前打开的文件描述符吗,进程是否允许对其进行写操作。如果有条件不满足,则返回负值,错误码-EBADF。

但是,有一类型的检查对于所有系统调用是通用的。每当参数指定地址时,内核必须检查是否在进程的地址空间中。检查有两种方法:

验证线性地址是否属于进程地址空间,如果是,包含该地址的内存是否有正确的访问权限。

仅验证该线性地址小于PAGE_OFFSET(也就是没有落在为内核保留的间隔地址范围内)。

早期的Linux执行第一种检查,但是这相当耗时,因为必须为系统调用中的每个地址参数执行检查;更重要的是,这通常是无用的,因为错误程序并不总是常见。

因此,从2.2版本开始,Linux采用了第2种方案。该方法非常高效,因为不用对进程所使用的内存区域描述符进行扫描。很明显,这是粗放式的检查:验证线性地址小于PAGE_OFFSET,是验证其正确性的必要条件但不是充分条件。但是,使用这方法并没有风险,因为后面会造成其它错误。

该方法将真正的检查推迟到了最后时刻,也就是说,物理分页单元将线性地址转换成物理地址的时候。我们将在后面的“动态地址检查:修复代码”中,阐述页错误异常处理程序如何检测到那些用户态传递给内核的错误地址(参数)。

此刻,可能有人会想为什么执行这种粗检查?因为这对于保护进程地址空间和内核地址空间的非法访问是至关重要的。第2章中我们得知,物理内存从线性地址PAGE_OFFSET开始映射。这意味着内核服务例程能够访问内存中的所有内存页。因此,如果不做粗检查,用户进程可能会传递属于内核地址空间的地址作为参数,然后,就能够访问这些内存而不会造成页错误异常。

对系统调用的地址参数进行检查可以使用access_ok()宏实现,主要作用于两个参数:addr和size。它会检查addr和addr + size - 1之前确定的间隔是否合理。本质上,等价于下面的C函数:

 

int access_ok(const void * addr, unsigned long size)
{
    unsigned long a = (unsigned long) addr;
    if (a + size < a ||
        a + size > current_thread_info()->addr_limit.seg)
        return 0;
    return 1;
}

 

该函数首先检查addr + size是否大于2^32-1,因为GNU C编译器(gcc)将无符号长整形和指针表示为32位数字,所以,相当于检查溢出。该函数还会检查addr + size是否超过了存储在current的``thread_info数据结构中的addr_limit.seg字段中的值。对于普通进程,该字段为PAGE_OFFSET;对于内核线程,该值为0xffffffff。该字段可以通过get_fs和set_fs`宏动态修改;这允许内核绕过安全检查,直接调用系统调用服务例程,直接将内核数据段中的地址传递给它们。

verify_area()可以执行access_ok()相同的功能。该函数已经废弃。

2 访问进程地址空间

系统服务例程经常需要读写进程地址空间中的数据。Linux提供了一组宏方便这种读写请求。下面我们描述其中的两个:get_user( )和put_user( )。前者用来从用户态地址空间读取1、2或4个连续字节,后者则是写入相同字节数。

函数具有两个参数x和变量ptr。ptr决定传输多少个字节。因此,在get_user(x,ptr)函数中,会根据ptr指向变量的大小将函数展开为__get_user_1()、__get_user_2()或__get_user_4()汇编函数中的一个。下面以__get_user_2()为例:

 

__get_user_2:
    /* 检查用户空间内存地址是否有效 */
    addl    $1, %eax            /* eax+1,即用户空间内存地址加1 */
    jc      bad_get_user        /* 如果进位标志位(carry)被设置,则跳转到bad_get_user */
                                /* 用于检查是否溢出 */
    movl    $0xffffe000, %edx   /* 栈大小设置为0xffffe000(4KB) */
    andl    %esp, %edx          /* 将栈指针寄存器按照边界对齐 */
    cmpl    24(%edx), %eax      /* 将栈基址+24,其所指向的内存地址与eax值进行比较 */
                                /* 用于检查线性地址是否小于addr_limit.seg */
    jae     bad_get_user        /* 如果线性地址≥addr_limit.seg,则跳转到bad_get_user */
2: movzwl   -1(%eax), %edx      /* eax中的地址减1,去除一个16位无符号数进行零扩展到32位 */
                                /* 将结果写入到edx中 */
    xorl %eax, %eax             /* 异或操作,清零eax */
    ret                         /* 返回到函数调用的下一条指令处 */
bad_get_user:
    /* 异常处理代码 */
    xorl %edx, %edx             /* 清零edx */
    movl $-EFAULT, %eax         /* 返回结果-EFAULT写入到eax寄存器 */
    ret                         /* 返回调用该函数的位置,结束函数的执行 */

 

寄存器eax包含要读取的第一个字节的地址。前6条指令本质上执行与access_ok()宏相同的检查:确保要读取的2个字节地址不会超过4G,也小于当前进程addr_limit.seg字段的限制。这个字段在thread_info结构体中的偏移量是24,这就是为什么cmpl指令的第一个操作数寻址加24的原因。

如果地址合法,则执行movzwl指令,将要读取的数据最低2个字节存储到edx寄存器,而edx寄存器的高位设置为0,然后返回0(eax寄存器清零)。如果地址不合法,则清零edx,返回-EFAULT错误码。

put_user(x,ptr)宏与get_user(x,ptr)类似,区别在于它是写入数据。依赖x的大小,它选择调用__put_user_asm( )宏(写入1、2、4字节),还是调用__put_user_u64()(写入8字节)。如果成功,这两个宏都返回0(写入eax寄存器),否则返回-EFAULT。

内核态下还有几个其它函数和宏可以访问用户进程地址空间,如表10-1所示。注意,它们都有一个以下划线(__)为前缀的变体。没有下划线的函数和宏会额外检查想要访问的线性地址块是否合法,而有下划线的则会绕过检查。尤其是当内核需要重复访问进程地址空间中同一片区域时,只在开始检查一遍地址更有效率。

表10-1 可以访问进程地址空间的函数和宏

函数 行为
get_user
__get_user
从用户空间读取一个整型值(1、2、4字节)
put_user
__put_user
写一个整型值到用户空间(1、2、4字节)
copy_from_user
__copy_from_user
从用户空间拷贝一块任意大小的数据
copy_to_user
__copy_to_user
拷贝一块任意大小的数据到用户空间
strncpy_from_user
__strncpy_from_user
从用户空间拷贝null结尾的字符串
strlen_user
strnlen_user
返回用户空间中的字符串长度(以NULL结尾)
clear_user
__clear_user
清空用户空间的一段内存区域(填0)

3 动态地址检查:修复代码

如前所示,access_ok()对系统调用的线性地址参数进行一个粗略检查。这可以保证用户进程不会伪造内核地址空间,但是,该线性地址仍然可能会不属于进程地址空间。这种情况下,Page Fault异常将会发生。

在描述内核如何检测这种类型错误之前,先让我们确定内核态下可能发生Page Fault异常的4种情况。Page Fault异常处理程序必须区分这些情况,因为要采取的操作是完全不同的。

内核访问的用户态内存帧不存在,或是一个只读页,这种情况下,页错误异常处理程序必须分配并初始化一个新内存帧。(参考第9章的按需分页和写时复制章节)

内核访问自己的内存页,但是相应的页表项还没有初始化(参考第9章的处理非连续内存区域访问)。这种情况下,内核必须在当前进程的页表中正确设置这些项。

某些内核函数有bug,会造成异常发生;或者,异常是由瞬时硬件错误导致的。当这种情况发生时,异常处理程序必须执行内核oops(参见第9章的在地址空间内处理错误地址一节)。

本章将要引入的情况:系统调用服务例程试图读写不属于进程地址空间的地址。

Page Fault异常处理程序可以通过确定错误的线性地址是否包含在进程所属的内存区域中,就可以轻松识别出第一种情况。通过检测相应的主内核页表项是否包含映射该地址的非空项即可。现在,让我们看看如何识别余下的两种情况。

4 异常表

确定Page Fault异常源的关键在于,内核可访问进程地址空间的可用调用非常少。前面我们已经描述了,仅有一组函数和宏可以用来访问用户进程地址空间。因此,如果异常是由无效参数引起的,则导致异常的指令必须包含在其中的一个函数或宏展开的代码中。所以说,处理用户空间的指令数量相当少。

因此,将访问进程地址空间的每个内核指令的地址放入到异常表中并不难。如果我们做到这一点,其它工作就很容易了。当Page Fault异常发生在内核态时,do_page_fault()异常处理程序会检查异常表:如果包含触发异常的指令,则错误就是由不合适的系统调用参数引起的;否则,可能就是由更严重的错误导致的。

Linux定义了几个异常表。主异常表是在构建内核镜像时由C编译器自动产生的。存储在内核代码段的__ex_table段中,起始地址分别是__start___ex_table和__stop___ex_table,符号是由C编译器产生的。

Linux 5.18.18中的链接文件定义(文件位置:include/asm-generic/vmlinux.lds.h):

 

/*
 * Exception table
 */
#define EXCEPTION_TABLE(align)                        
    . = ALIGN(align);                                 
    __ex_table : AT(ADDR(__ex_table) - LOAD_OFFSET) { 
        __start___ex_table = .;                       
        KEEP(*(__ex_table))                           
        __stop___ex_table = .;                        
    }

 

更重要的是,内核中动态加载的模块包含了自己独立的异常表。这个异常表是在构建模块镜像时由C编译器自动产生的,当模块被插入到正在运行的内核中时,它会被加载到内存中。

异常表的每一项,都是一个类型为exception_table_entry的数据结构,包含两个域:

insn

访问进程地址空间指令的线性地址。

fixup

执行修正的汇编代码的地址。insn指令触发Page Fault异常时,调用这些汇编代码。

修复代码由一些汇编指令组成,用于解决由异常触发的问题。正如我们将在本节后面看到的,修复代码通常由一系列指令组成,这些指令强制服务例程向用户态进程返回错误码。这些指令通常定义在访问进程地址空间的同一个宏或函数中,由C编译器放置在称为.fixup的内核代码段的独立部分中。

search_exception_tables()函数用于在所有异常表中搜索指定的地址:如果该地址包含在表中,则该函数返回指向相应exception_table_entry结构的指针;否则,返回NULL。因此,Page Fault处理程序do_page_fault()执行以下语句:

 

if ((fixup = search_exception_tables(regs->eip))) {
    regs->eip = fixup->fixup;
    return 1;
}

 

正常情况下,regs->eip指向异常发生时内核态栈上保存的eip寄存器值。如果寄存器中的值(发生异常的指令地址)在异常表中,do_page_fault()则用search_exception_tables()返回的表项中的地址替换寄存器中的值。然后,Page Fault处理程序终止,被中断的程序继续执行修复代码。

5 产生异常表和修复代码

GNU Assembler的.section指令允许编程者指定可执行文件的哪个section包含后面跟随的代码。正如我们将在第20章看到的,一个可执行文件可以包含一个代码segment,继而,它又可以被分成几个代码section。因此,下面的代码是将一个表项添加到异常表中;"a"属性标识该代码section必须和内核镜像的其余部分一起加载到内存中:

 

.section __ex_table, "a"
    .long faulty_instruction_address, fixup_code_address
.previous

 

.previous指令用于回到之前的位置,也就是离开__ex_table代码段的定义,继续处理之前的代码。

让我们再次考虑前面提到的__get_user_1()、__get_user_2()和__get_user_4()函数。真正访问进程地址空间的指令是那些标记为1、2和3处的汇编指令:

 

__get_user_1:
    [...]
1: movzbl (%eax), %edx
    [...]
__get_user_2:
    [...]
2: movzwl -1(%eax), %edx
    [...]
__get_user_4:
    [...]
3: movl -3(%eax), %edx
    [...]
bad_get_user:
    xorl %edx, %edx
    movl $-EFAULT, %eax
    ret
.section __ex_table,"a"
    .long 1b, bad_get_user
    .long 2b, bad_get_user
    .long 3b, bad_get_user
.previous

 

Linux 5.18.18中的主要逻辑还是这样,但是进行了代码封装。

每个异常表项由2个标签组成。第1个是带有b后缀的数字标签,表示标签是backward,表示标签处的代码在最近的前面。修复代码对于这3个函数是通用的,标记为bad_get_user。如果标签1、2或3处的汇编指令产生Page Fault异常,则执行修复代码。此处,仅仅是向发起系统调用的进程返回一个-EFAULT错误码。

查看作用于用户地址空间的其它内核函数使用的修复代码技术。例如,strlen_user(string)宏。该宏返回系统调用传递的一个以null结尾的字符串长度,如果发生错误,返回0。该宏实际产生以下汇编代码:

 

    movl $0, %eax           ; 将eax设置为0, 作为计数器的初始值
    movl $0x7fffffff, %ecx  ; 将ecx设置为0x7fffffff, 即2^31-1, 即最大的有符号整数值
    movl %ecx, %ebx         ; 将ecx值拷贝到ebx中
    movl string, %edi       ; 将字符串string的地址赋值给edi寄存器
0: repne; scasb             ; 执行重复动作,将edi指向的内存地址开始和累加器eax的值比较,
                            ; 直到匹配到eax或ecx达到零。这个操作的目的是找到字符串中的
                            ; NULL字节,也就是字符串的结尾
    subl %ecx, %ebx         ; 用计数器ecx的值减去计数器ebx的值,得到字符串长度
    movl %ebx, %eax         ; 将计数器ebx的值(也就是字符串的长度)复制到累加器eax中
1:
.section .fixup,"ax"        ; 定义了一个为.fixup的代码段,并指定属性为`ax`
2: xorl %eax, %eax          ; 异或操作,寄存器清零
    jmp 1b                  ; 跳转到标签为1的代码处
.previous                   
.section __ex_table,"a"     ; 定义了一个异常表, __ex_table, 属性为a
    .long 0b, 2b            ; 将标签0和标签2处的修复代码地址存放到异常表项中
.previous

 

说明:

repne是重复执行指令

scas是用来搜索字符,后缀b表示按字节搜索

寄存器ecx和ebx被初始化为0x7fffffff,表示用户态地址空间中字符串允许的最大长度。repne;scasb汇编指令迭代扫描edi指向的字符串,并寻找eax寄存器中的0值(也就是字符串�结束符)。因为scasb每次迭代都会减小ecx的值,所以eax最终存储了字符串总字节数(也就是字符串长度)。

该宏的修复代码被插入到.fixup段中。ax属性表示该代码段被加载到内存中且包含可执行代码。如果标签0的指令产生Page Fault异常,则执行修复代码;修复代码也仅仅是返回了错误值0,而没有返回字符串长度,然后就跳转到了标签1处,标签1后面对应的是宏后面的代码。

第二个.section指令是将repne;scasb指令的地址和fixup代码地址加入到异常表__ex_table所在的代码段中。

6 架构调用约定

EABI和OABI

ABI是应用程序二进制接口,每个OS都会为运行在该OS的应用程序提供ABI。ABI包含了应用程序在这个OS下运行时必须遵守的编程约定。对于ARM架构而言,它定义了函数调用约定、系统调用形式以及目标文件格式等。

在ARM架构中,存在两种不同的ABI形式,OABI和EABI,OABI中的O是old的意思,表示旧有的ABI,而EABI是基于OABI上的改进,或者说它更适合目前大多数的硬件,OABI和EABI的区别主要在于浮点的处理和系统调用。浮点的区别不做过多讨论,对于系统调用而言,OABI和EABI最大的区别在于,OABI 的系统调用指令需要传递参数来指定系统调用号,而EABI中将系统调用号保存在r7中。

架构特定要求

每种结构ABI对于如何将系统调用参数传递到内核都有自己的要求。对于具有glibc封装的系统调用(例如,大多数系统调用),glibc以适合架构的方式处理将参数复制到正确寄存器。然而,当使用syscall()进行系统调用时,调用者可能需要处理依赖于体系结构的细节;此要求在某些32位架构上最常遇到。

例如,对于ARM架构EABI,64位值(例如,long long)必须与偶数寄存器对对齐。因此,使用syscall()而不是glibc提供的封装函数,readahead(2)系统调用将在ARM架构上以小端模式调用EABI,如下所示:

 

syscall(SYS_readahead, fd, 0,
    (unsigned int) (offset & 0xFFFFFFFF),
    (unsigned int) (offset >> 32),
    count);

 

因为offset是64位,所以,fd占用了r0,填充0到r1,然后调用者需要手动将offset切割,以便将其存放到r2/r3这一对寄存器中。还需要注意大小端格式(依赖于平台使用的C ABI约定)。

架构调用约定

每种架构都有调用和向内核传递参数的方式。细节可以参考下面的两个表。

第一张表列出了转换到内核态的调用指令(这可能不是最好或最快的方式,参考vdso机制),表示系统调用号的寄存器,表示返回系统调用结果的寄存器,以及表示发送错误信号的寄存器。

Arch/ABI 指令 调用号 返回值 返回值 错误 注释
alpha callsys v0 v0 a4 a3 1, 6
arc trap0 r8 r0 - -  
arm/OABI swi NR - r0 - - 2
arm/EABI swi 0x0 r7 r0 r1 -  
arm64 svc #0 w8 x0 x1 -  
blackfin excpt 0x0 P0 R0 - -  
i386 int $0x80 eax eax edx -  
ia64 break 0x100000 r15 r8 r9 r10 1, 6
loongarch syscall 0 a7 a0 - -  
m68k trap #0 d0 d0 - -  
microblaze brki r14,8 r12 r3 - -  
mips syscall v0 v0 v1 a3 1, 6
nios2 trap r2 r2 - r7  
parisc ble 0x100(%sr2, %r0) r20 r28 - -  
powerpc sc r0 r3 - r0 1
powerpc64 sc r0 r3 - cr0.SO 1
riscv ecall a7 a0 a1 -  
s390 svc 0 r1 r2 r3 - 3
s390x svc 0 r1 r2 r3 - 3
superh trapa #31 r3 r0 r1 - 4, 6
sparc/32 t 0x10 g1 o0 o1 psr/csr 1, 6
sparc/64 t 0x6d g1 o0 o1 psr/csr 1, 6
tile swint1 R10 R00 - R01 1
x86-64 syscall rax rax rdx - 5
x32 syscall rax rax rdx - 5
xtensa syscall a2 a2 - -  

注意:

有些架构中,可能会选择一个寄存器当作布尔值使用(0表示无错误,-1表示错误),通过这种方式告知系统调用失败。真正的错误值仍然存储在返回寄存器中。在sparc架构上,处理器状态寄存器psr中的进位标志位csr被用作一个完整寄存器使用。在powerpc64架构中,条件寄存器cr0中字段0中的加法溢出标志位(SO)会被当做错误寄存器使用。

NR是系统调用号

对于s390和s390x,如果系统调用号小于256,可能会直接通过svc NR传递。

对于SuperH架构,因为历史原因支持额外的陷阱号,但是trapa #31是推荐使用的。

x32和x86-64共享系统调用表,但是有细微差别。

某些架构(如 Alpha、IA-64、MIPS、SuperH、sparc/32、sparc/64)使用了额外的寄存器(返回值第二列),用其从pipe(2)系统调用中返回第二个返回值;Alpha还在系统调用getxpid(2)、getxuid(2)、getxgid(2)中使用这种技术。其它架构没有使用第二个返回寄存器,即使在System V ABI定义了相关寄存器。

第二个表:传递系统调用参数的寄存器约定

Arch/ABI arg1 arg2 arg3 arg4 arg5 arg6 arg7 注释
alpha a0 a1 a2 a3 a4 a5 -  
arc r0 r1 r2 r3 r4 r5 -  
arm/OABI r0 r1 r2 r3 r4 r5 r6  
arm/EABI r0 r1 r2 r3 r4 r5 r6  
arm64 x0 x1 x2 x3 x4 x5 -  
blackfin R0 R1 R2 R3 R4 R5 -  
i386 ebx ecx edx esi edi ebp -  
ia64 out0 out1 out2 out3 out4 out5 -  
loongarch a0 a1 a2 a3 a4 a5 a6  
m68k d1 d2 d3 d4 d5 a0 -  
microblaze r5 r6 r7 r8 r9 r10 -  
mips/o32 a0 a1 a2 a3 - - - 1
mips/n32,64 a0 a1 a2 a3 a4 a5 -  
nios2 r4 r5 r6 r7 r8 r9 -  
parisc r26 r25 r24 r23 r22 r21 -  
powerpc r3 r4 r5 r6 r7 r8 r9  
powerpc64 r3 r4 r5 r6 r7 r8 -  
riscv a0 a1 a2 a3 a4 a5 -  
s390 r2 r3 r4 r5 r6 r7 -  
s390x r2 r3 r4 r5 r6 r7 -  
superh r4 r5 r6 r7 r0 r1 r2  
sparc/32 o0 o1 o2 o3 o4 o5 -  
sparc/64 o0 o1 o2 o3 o4 o5 -  
tile R00 R01 R02 R03 R04 R05 -  
x86-64 rdi rsi rdx r10 r8 r9 -  
x32 rdi rsi rdx r10 r8 r9 -  
xtensa a6 a3 a4 a5 a8 a9 -  

注释:mips/o32在用户栈上传递5~8参数

  审核编辑:汤梓红

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分