assembly 在Linux内核中断处理程序中传递函数参数(从asm到C)

yws3nbqq  于 2023-10-19  发布在  Linux
关注(0)|答案(2)|浏览(112)

当我阅读Linux内核源代码时,我遇到了这段代码:

__visible void __irq_entry smp_apic_timer_interrupt(struct pt_regs *regs)   
{
    struct pt_regs *old_regs = set_irq_regs(regs);

    entering_ack_irq();
    local_apic_timer_interrupt();
    exiting_irq();

    set_irq_regs(old_regs);
}

函数smp_apic_timer_interrupt()接受一个参数。这个函数的调用是由一段汇编语言代码完成的:

ENTRY(apic_timer_interrupt)
     RING0_INT_FRAME;     
     ASM_CLAC;           
     pushl_cfi $~(0xef);
     SAVE_ALL;         
     TRACE_IRQS_OFF
     movl %esp,%eax;
     call smp_apic_timer_interrupt; // <------call high level C function       
     jmp ret_from_intr;      
     CFI_ENDPROC;           
ENDPROC(apic_timer_interrupt)

我不知道高级 C 函数smp_apic_timer_interrupt()是如何获取其参数的(通过哪个寄存器)?

azpvetkf

azpvetkf1#

你可能在考虑正常的调用约定(堆栈上的参数)。现代Linux内核(32位变体)将寄存器中的前3个参数(EAXEDXECX,按此顺序)作为优化。根据内核的不同,这个约定被指定为使用__attribute__(regparm(3))的函数的属性修饰符,或者内核的现代版本在命令行上将-mregparm=3选项传递给 GCCGCCdocumentation是这样描述这个选项/属性的:

regparm (number)
On the Intel 386, the regparm attribute causes the compiler to pass up to
number integer arguments in registers EAX, EDX, and ECX instead of on the
stack. Functions that take a variable number of arguments will continue to
be passed all of their arguments on the stack.

在古老的内核中,正常的32位ABI(以及堆栈上参数的约定)是标准。最终,内核配置通过内核构建配置中的 CONFIG_REGPARM 设置支持寄存器中的参数 * 或 * 正常堆栈约定:

config REGPARM
bool "Use register arguments"
    default y
    help
    Compile the kernel with -mregparm=3. This instructs gcc to use
    a more efficient function call ABI which passes the first three
    arguments of a function call via registers, which results in denser
    and faster code.
If this option is disabled, then the default ABI of passing
    arguments via the stack is used.
If unsure, say Y.

Linux内核维护者在2006年用这个kernel commit去掉了这个选项:

-mregparm=3 has been enabled by default for some time on i386, and AFAIK
there aren't any problems with it left.
This patch removes the REGPARM config option and sets -mregparm=3
unconditionally.

基于这些知识,我们可以查看您提供的代码,并假设我们在内核上,默认前3个参数传入寄存器。在您的案例中:

__visible void __irq_entry smp_apic_timer_interrupt(struct pt_regs *regs)

有 * 一个 * 参数,所以它在 EAX 中传递。调用 smp_apic_timer_interrupt 的代码如下所示:

ENTRY(apic_timer_interrupt)
     RING0_INT_FRAME;     
     ASM_CLAC;           
     pushl_cfi $~(0xef);
     SAVE_ALL;         
     TRACE_IRQS_OFF
     movl %esp,%eax;
     call smp_apic_timer_interrupt; // <------call high level C function       
     jmp ret_from_intr;      
     CFI_ENDPROC;           
ENDPROC(apic_timer_interrupt)

重要的部分是保存_ALL宏调用将所有必需的寄存器压入堆栈。它会因内核的版本而异,但将寄存器推入堆栈的主要效果是相似的(为了简洁起见,我删除了 DWARF 条目):

.macro SAVE_ALL
         cld
         PUSH_GS
         pushl_cfi %fs
         pushl_cfi %es
         pushl_cfi %ds
         pushl_cfi %eax
         pushl_cfi %ebp
         pushl_cfi %edi
         pushl_cfi %esi
         pushl_cfi %edx
         pushl_cfi %ecx
         pushl_cfi %ebx
         movl $(__USER_DS), %edx
         movl %edx, %ds
         movl %edx, %es
         movl $(__KERNEL_PERCPU), %edx
         movl %edx, %fs
         SET_KERNEL_GS %edx
.endm

完成后,ESP 将指向最后一个寄存器被推入的位置。该地址通过movl %esp,%eax复制到 EAXEAX 成为struct pt_regs *regs的指针。堆栈上所有被压入的寄存器都变成了实际的 *pt_blog * 数据结构,EAX 现在指向它。
asmlinkage宏可以在内核中找到,用于那些需要以传统方式在堆栈上传递参数的函数。它的定义如下:

#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))

其中regparm(0)表示不会通过寄存器传递任何参数。
人们真的必须知道构建选项是什么,以及使用的内核版本,以便对使用的约定进行准确的评估。

bwntbbo3

bwntbbo32#

引用自https://www.safaribooksonline.com/library/view/understanding-the-linux/0596005652/ch04s06.html
SAVE_ALL宏扩展为以下片段:

cld
push %es
push %ds
pushl %eax
pushl %ebp
pushl %edi
pushl %esi
pushl %edx
pushl %ecx
pushl %ebx
movl $ _ _USER_DS,%edx
movl %edx,%ds
movl %edx,%es

保存寄存器后,当前栈顶位置的地址保存在eax寄存器中[与movl %esp,%eax一起,以便] eax指向包含SAVE_ALL推入的最后一个寄存器值的栈位置
因此,eax寄存器是smp_apic_timer_interrupt接收pt_regs指针的寄存器。

相关问题