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小时内删除。