简体   繁体   English

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

[英]Function parameter passing in a Linux kernel interrupt handler (from asm to C)

When I read the Linux kernel source, I came across this piece of code: 当我阅读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);
}

The function smp_apic_timer_interrupt() takes one parameter. 函数smp_apic_timer_interrupt()接受一个参数。 The calling of this function is by a piece of assembly language code: 调用此函数是通过一段汇编语言代码:

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)

I cannot figure out how the high level C function smp_apic_timer_interrupt() get its parameter (by which register)? 我无法弄清楚高级C函数smp_apic_timer_interrupt()获取其参数(通过哪个寄存器)?

Your are probably thinking normal calling convention (arguments on the stack). 您可能正在考虑正常的调用约定(堆栈上的参数)。 Modern Linux kernels (32-bit variants) pass the first 3 parameters in registers ( EAX , ECX , EDX ) as an optimization. 现代Linux内核(32位变体)将寄存器中的前3个参数( EAXECXEDX )作为优化传递。 Depending on the kernel this convention is specified as an attribute modifier on the functions using __attribute__(regparm(3)) , or modern versions of the kernel pass -mregparm=3 option to GCC on the command line. 根据内核,此约定在函数上使用__attribute__(regparm(3))指定为属性修饰符,或者在命令行-mregparm=3内核的现代版本传递给GCC -mregparm=3选项。 The GCC documentation says this about that option/attribute: GCC 文档说明了这个选项/属性:

 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. 

In ancient kernels the normal 32-bit ABI (and convention of arguments on the stack) was the norm. 在古代内核中,正常的32位ABI(以及堆栈上的参数约定)是常态。 Eventually the kernel configuration supported arguments in registers OR the normal stack convention via the CONFIG_REGPARM setting in the kernel build configuration: 最终,内核配置通过内核构建配置中的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. 

The Linux kernel maintainers got rid of this option in 2006 with this kernel commit : Linux内核维护者在2006年通过此内核提交摆脱了这个选项:

 -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. 

Based on this knowledge one can look at the code you have presented and assume we are on a kernel where it has defaulted to the first 3 parameters being passed in registers. 基于这些知识,您可以查看您提供的代码,并假设我们在内核中默认为在寄存器中传递的前3个参数。 In your case: 在你的情况下:

 __visible void __irq_entry smp_apic_timer_interrupt(struct pt_regs *regs)

has one parameter so it is passed in EAX . 一个参数,所以它在EAX中传递。 The code that called smp_apic_timer_interrupt looked like this: 调用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)

The important part is that the SAVE_ALL macro call pushes all the required registers on the stack. 重要的是SAVE_ALL宏调用将所有必需的寄存器推送到堆栈上。 It will vary from version to version of the kernel, but the main effect of pushing registers on the stack is similar (I've removed the DWARF entries for brevity): 它会因内核的版本而异,但是在堆栈上推送寄存器的主要作用是相似的(为简洁起见,我删除了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

When completed ESP will point to the location where the last register was pushed. 完成后, ESP将指向推送最后一个寄存器的位置。 That address is copied to EAX with movl %esp,%eax , and EAX becomes the pointer for struct pt_regs *regs . 该地址使用movl %esp,%eax复制到EAXEAX成为struct pt_regs *regs的指针。 All the pushed registers on the stack become the actual pt_regs data structure, and EAX now points to it. 堆栈上所有推送的寄存器都成为实际的pt_regs数据结构, EAX现在指向它。

The asmlinkage macro will be found in the kernel for those functions that require arguments to be passed on the stack the conventional way. asmlinkage宏将在内核中找到那些需要以传统方式在堆栈上传递参数的函数。 It is defined as something like: 它定义为:

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

Where regparm(0) says that no parameters will be passed via registers. regparm(0)表示没有参数通过寄存器传递。

One really has to know what the build options are, and the version of the kernel being used to make an accurate assessment of the convention being used. 我们必须知道构建选项是什么,以及用于对所使用的约定进行准确评估的内核版本。

Quoting from https://www.safaribooksonline.com/library/view/understanding-the-linux/0596005652/ch04s06.html 引自https://www.safaribooksonline.com/library/view/understanding-the-linux/0596005652/ch04s06.html

The SAVE_ALL macro expands to the following fragment: 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 

After saving the registers, the address of the current top stack location is saved in the eax register [with movl %esp,%eax , so that ] eax points to the stack location containing the last register value pushed on by SAVE_ALL 保存寄存器后,当前顶部堆栈位置的地址保存在eax寄存器中[使用movl %esp,%eax ,这样] eax指向包含SAVE_ALL推送的最后一个寄存器值的堆栈位置

So the eax register is the register through which smp_apic_timer_interrupt receives the pt_regs pointer. 因此, eax寄存器是smp_apic_timer_interrupt通过其接收pt_regs指针的寄存器。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM