3.4.2 在汇编程序中调用C函数
从汇编程序中调用C语言函数的方法实际上在上面已经给出。在上面C语言例子对应的汇编程序代码中,我们可以看出汇编程序语句是如何调用swap()函数的。现在我们对调用方法作一总结。
在汇编程序调用一个C函数时,程序需要首先按照逆向顺序把函数参数压入栈中,即函数最后(最右边的)一个参数先入栈,而最左边的第1个参数在最后调用指令之前入栈,如图3-6所示。然后执行CALL指令去执行被调用的函数。在调用函数返回后,程序需要再把先前压入栈中的函数参数清除掉。
调用函数时压入堆栈的参数 |
在执行CALL指令时,CPU会把CALL指令的下一条指令的地址压入栈中(见图3-6中的EIP)。如果调用还涉及代码特权级变化,那么CPU会进行堆栈切换,并且把当前堆栈指针、段描述符和调用参数压入新堆栈中。由于Linux内核中只使用中断门和陷阱门方式处理特权级变化时的调用情况,并没有使用CALL指令来处理特权级变化的情况,因此这里对特权级变化时的CALL指令使用方式不再进行说明。
汇编中调用C函数比较"自由",只要是在栈中适当位置的内容就都可以作为参数供C函数使用。这里仍然以图3-6中具有3个参数的函数调用为例,如果我们没有专门为调用函数func()压入参数就直接调用它的话,那么func()函数仍然会把存放EIP位置以上的栈中其他内容作为自己的参数使用。如果我们为调用func()而仅仅明确地压入了第1、第2个参数,那么func()函数的第3个参数p3就会直接使用p2前的栈中内容。在Linux 0.1x内核代码中就有几处使用了这种方式。例如在kernel/sys_call.s汇编程序中第231行上调用copy_process()函数(kernel/fork.c中第68行)的情况。在汇编程序函数_sys_fork中虽然只把5个参数压入了栈中,但是copy_process()却带有多达17个参数(见下面的程序)。
// kernel/sys_call.s汇编程序_sys_fork部分。 226push %gs 227pushl %esi 228pushl %edi 229pushl %ebp 230pushl %eax 231call _copy_process # 调用C函数copy_process()(kernel/fork.c,68)。 232addl $20,%esp # 丢弃这里所有压栈内容。 233 1: ret // kernel/fork.c程序。 write的返回值68 int copy_process(int nr,long ebp,long edi,long esi,long gs,long none, 69 long ebx,long ecx,long edx, long orig_eax, 70 long fs,long es,long ds, 71 long eip,long cs,long eflags,long esp,long ss) |
我们知道,参数越是最后入栈,越是靠近C函数参数左侧。因此实际上调用copy_process()函数之前入栈5个寄存器值就是copy_process()函数的最左面的5个参数。按顺序它们分别对应为入栈的eax(nr)、ebp、edi、esi和寄存器gs的值。而随后的其余参数实际上直接对应堆栈上已有的内容。这些内容是从进入系统调用中断处理过程开始,直到调用本系统调用处理过程时逐步入栈的各寄存器的值。
参数none是sys_call.s程序第99行上利用地址跳转表sys_call_table[](定义在include/linux/ sys.h,93行)调用_sys_fork时的下一条指令的返回地址值。随后的参数是刚进入system_call时在85~91行压入栈的寄存器ebx、ecx、edx、原eax和段寄存器fs、es、ds。最后5个参数是CPU执行中断指令压入返回地址eip和cs、标志寄存器eflags、用户栈地址esp和ss。因为系统调用涉及程序特权级变化,所以CPU会把标志寄存器值和用户栈地址也压入堆栈。在调用C函数copy_process()返回后,_sys_fork也只把自己压入的5个参数丢弃掉,栈中其他值均保存着。其他采用上述用法的函数还有kernel/signal.c中的do_signal()、fs/exec.c中的do_execve()等,请读者自行分析。
另外,我们说汇编程序调用C函数比较自由的另一个原因是我们可以根本不用CALL指令而
采用JMP指令来同样达到调用函数的目的。方法是在参数入栈后把下一条要执行的指令地址人工压入栈中,然后直接使用JMP指令跳转到被调用函数开始地址处去执行函数。此后当函数执行完成时就会执行RET指令,把人工压入栈中的下一条指令地址弹出,作为函数返回的地址。Linux内核中也有多处用到了这种函数调用方法,例如kernel/asm.s程序第62行调用执行traps.c中的do_int3()函数的情况。
3.4.3 在C程序中调用汇编函数
从C程序中调用汇编程序函数的方法与汇编程序中调用C函数的原理相同,但Linux 内核程序中不常使用。调用方法的着重点仍然是对函数参数在栈中位置的确定上。当然,如果调用的汇编语言程序比较短,那么可以直接在C程序中使用上面介绍的内联汇编语句来实现。下面举例说明编制这类程序的方法。包含两个函数的汇编程序callee.s如下。
/* 本汇编程序利用系统调用sys_write()实现显示函数int mywrite(int fd, char * buf, int count)。 函数int myadd(int a, int b, int * res) 用于执行 a+b = res运算。若函数返回0,则说明溢出。 注意:如果在现在的Linux系统(如RedHat 9)下编译,则请 去掉函数名前的下划线'_'。 */ SYSWRITE = 4 # sys_write()系统调用号。 .global _mywrite, _myadd .text _mywrite: pushl %ebp movl %esp, %ebp pushl %ebx movl 8(%ebp), %ebx # 取调用者第1个参数:文件描述符fd。 movl 12(%ebp), %ecx # 取第2个参数:缓冲区指针。 movl 16(%ebp), %edx # 取第3个参数:显示字符数。 movl $SYSWRITE,%eax # %eax中放入系统调用号4。 int $0x80 # 执行系统调用。 popl %ebx movl %ebp, %esp popl %ebp ret _myadd: pushl %ebp movl %esp, %ebp movl 8(%ebp), %eax # 取第1个参数a。 movl 12(%ebp), %edx # 取第2个参数b。 xorl %ecx, %ecx # %ecx为0表示计算溢出。 addl %eax, %edx # 执行加法运算。 jo 1f # 若溢出则跳转。 movl 16(%ebp), %eax # 取第3个参数的指针。 movl %edx, (%eax) # 把计算结果放入指针所指位置处。 incl %ecx # 没有发生溢出,于是设置无溢出返回值。 1:movl %ecx, %eax # %eax中是函数返回值。 movl %ebp, %esp popl %ebp ret |
该汇编文件中的第1个函数mywrite()利用系统中断0x80调用系统调用sys_write(int fd, char *buf, int count)实现在屏幕上显示信息。对应的系统调用功能号是4(参见include/ unistd.h),3个参数分别为文件描述符、显示缓冲区指针和显示字符数。在执行int 0x80之前,寄存器%eax中需要放入调用功能号(4),寄存器%ebx、%ecx和%edx要按调用规定分别存放fd、buf和count。函数mywrite()的调用参数个数和用途与sys_write()完全一样。
第2个函数myadd(int a, int b, int *res)执行加法运算。其中参数res是运算的结果。函数返回值用于判断是否发生溢出。如果返回值为0表示计算已发生溢出,结果不可用。否则计算结果将通过参数res返回给调用者。
注意:如果在现在的Linux系统(如RedHat 9)下编译callee.s程序,则请去掉函数名前的下画线"_"。调用这两个函数的C程序caller.c如下所示。
/* 调用汇编函数mywrite(fd, buf, count)显示信息; 调用myadd(a, b, result)执行加运算。 如果myadd()返回0,则表示加函数发生溢出。首先显示 开始计算信息,然后显示运算结果。 */ 01 int main() 02 { 03char buf[1024]; 04int a, b, res; 05char * mystr = "\n"; 06char * emsg = "Error in adding\n"; 07 08a = 5; b = 10; 09mywrite(1, mystr, strlen(mystr)); 10if (myadd(a, b, &res)){ 11 sprintf(buf, "The result is %d\n", res); 12 mywrite(1, buf, strlen(buf)); 13} else { 14 mywrite(1, emsg, strlen(emsg)); 15} 16return 0; 17 } |
该函数首先利用汇编函数mywrite()在屏幕上显示开始计算的信息"",然后调用加法计算汇编函数myadd()对a和b两个数进行运算,并在第3个参数res中返回计算结果。最后利用mywrite()函数把格式化的结果信息字符串显示在屏幕上。如果函数myadd()返回0,则表示加函数发生溢出,计算结果无效。这两个文件的编译和运行结果如下:
[/usr/root]# as -o callee.o callee.s [/usr/root]# gcc -o caller caller.c callee.o [/usr/root]# ./caller The result is 15 [/usr/root]# |
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论