当前位置:文档之家› 函数调用过程

函数调用过程


高级语言反汇编程序的函数调用过程
减小字体 增大字体 作者:佚名 来源:https://www.doczj.com/doc/b310377909.html, 发布时间:2006-7-19 6:34:27
Jim Chan

摘要:本文说明高级语言编译成汇编语言后,高级语言中函数调用的汇编程序过程。

正文:高级语言编译成汇编程序以后,在高级语言中的函数调用的汇编程序过程如下:

1.将函数参数入栈,第一个参数在栈顶,最后一个参数在栈底。

2.执行CALL指令,调用该函数,进入该函数代码空间。
a.执行CALL指令,将CALL指令下一行代码的地址入栈。
b.进入函数代码空间后,将基址指针EBP入栈,然后让基址指针EBP指向当前堆栈栈顶,并使用它访问存在堆栈中的函数输入参数及堆栈中的其他数据。
c.堆栈指针ESP减少一个值,如44H,向上移动一个距离,留出一个空间给该函数作为临时存储区。
{
// 以上准备工作做好后,函数正式被执行,如下所示。
d.将其他指针或寄存器中的值入栈,以便在函数中使用这些寄存器。
e.执行代码。
f.执行return()返回执行结果,将要返回的值存入EAX中。
g.步骤2.d中的指针出栈。
}
h.将EBP的值传给堆栈指针ESP,使ESP复原为2.c之前的值。此时进入函数时EBP的值在栈顶。
i.基址指针EBP出栈,复原为2.b之前的EBP的值。
j.执行RET指令,“调用函数”的地址出栈,本函数返回到CALL指令的下一行。

3.函数返回到CALL指令下一行,将堆栈指针加一个数值,以使堆栈指针恢复到以上步骤1执行之前的值。该数值是上面第一步入栈参数的总长度。

注意:
1.堆栈指针ESP指向栈顶的新入栈数据的最低位。
2.MOV指令中偏移指针指向被“MOV”的数据的最低位。如下面指令是将ebp+8到ebp+11四个字节的内容传到eax寄存器中。
00402048 mov eax,dword ptr [ebp+8]

一个例子如下:

高级语言代码中的函数调用如下:

117: bR = t1(p);

汇编代码如下:

00401FB8 mov ecx,dword ptr [ebp-8] ;将参数放入ecx寄存器
00401FBB push ecx ;参数入栈
00401FBC call @ILT+10(t1) (0040100f) ;函数调用,下一行地址00401FC1入栈
00401FC1 add esp,4 ;函数返回,堆栈指针加4,复原为00401FB8时的值
00401FC4 mov dword ptr [ebp-10h],eax ;从eax中取出高级语言中的函数返回值,放入bR变量中

其中t1函数如下:

125: BOOL t1(void* p)
126: {
00402030 push ebp ;ebp入栈
00402031 mov ebp,esp ;ebp指向此时堆栈的栈顶
00402033 sub esp,44h ;esp减少一个值,空出一段存储区
00402036 push ebx ;将三个寄存器的值入栈,以便在函数中使用它
00402037 push esi

;
00402038 push edi ;
00402039 lea edi,[ebp-44h] ;
0040203C mov ecx,11h ;
00402041 mov eax,0CCCCCCCCh ;
00402046 rep stos dword ptr [edi] ;
127: int* q = (int*)p; ;
00402048 mov eax,dword ptr [ebp+8] ;ebp+8指向函数输入参数的最低位地址;
;如果是ebp+4则指向函数返回地址00401FC1的最低位,值为C1
0040204B mov dword ptr [ebp-4],eax ;
128: return 0; ;
0040204E xor eax,eax ;返回值放入eax寄存器中
129: }
00402050 pop edi ;三个寄存器出栈
00402051 pop esi ;
00402052 pop ebx ;
00402053 mov esp,ebp ;esp复原
00402055 pop ebp ;ebp出栈,它的值也复原了
00402056 ret ;返回到此时栈顶存储的代码地址:00401FC1
;故而如果不幸被修改了返回地址,程序就会出现意外

以上汇编代码由VC++6.0编译得到。

堆栈在EBP入栈后的情况:

低位 高位
↓ ↓
内存地址 堆栈
┆ ┆
0012F600├──────┤← edi = 0012F600
│ │
0012F604├─┄┄┄┄─┤
│ │
│ │
┆ 44h的空间 ┆
┆ ┆
│ │
│ │
0012F640├─┄┄┄┄─┤
│ │
0012F644├──────┤← ebp被赋值后指向该单元,此时ebp=0012F644
│AC F6 12 00 │ebp赋值为esp之前的值
0012F648├──────┤
│C1 1F 40 00 │返回地址
0012F64C├──────┤← ebp + 8
│A0 F6 12 00 │函数实参p的值
0012F650├──────┤
│ │
├──────┤
┆ ┆

注:存储器存储空间堆栈按从高到低的排列,左边标注的地址是其右下方存储单元的最低位地址。如0012F644指向0012F6AC的AC字节,AC在栈顶。图中存储器中的内容按从低到高位书写,“AC F6 12 00”= 0x0012F6AC
























系统调用及参数传递过程
责任编辑:admin 更新日期:2005-8-6

1.系统调用函数接口是如何转化为陷入命令
系统调用是通过一条陷入指令进入核心态,然后根据传给核心的系统调用号为索引在系统调用表中找到相映的处理函数入口地址。这里将详细介绍这一过程。
我们以x86为例说明:
由于陷入指令是一条特殊指令,而且依赖与操作系统实现的平台,如在x86中,这条指令是int 0x80,这显然不是用户在编程时应该

使用的语句,因为这将使得用户程序难于移植。所以在操作系统的上层需要实现一个对应的系统调用库,每个系统调用都在该库中包含了一个入口点(如我们看到的fork, open, close等等),这些函数对程序员是可见的,而这些库函数的工作是以对应系统调用号作为参数,执行陷入指令int 0x80,以陷入核心执行真正的系统调用处理函数。当一个进程调用一个特定的系统调用库的入口点,正如同它调用任何函数一样,对于库函数也要创建一个栈帧。而当进程执行陷入指令时,它将处理机状态转换到核心态,并且在核心栈执行核心代码。

这里给出一个示例(linux/include/asm/unistd.h):
#define _syscallN(type, name, type1, arg1, type2, arg2, . . . ) \
type name(type1 arg1,type2 arg2) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2))); \
. . . . . .
__syscall_return(type,__res); \
}
在执行一个系统调用库中定义的系统调用入口函数时,实际执行的是类似如上的一段代码。这里牵涉到一些gcc的嵌入式汇编语言,不做详细的介绍,只简单说明其意义:
其中__NR_##name是系统调用号,如name == ioctl,则为__NR_ioctl,它将被放在寄存器eax中作为参数传递给中断0x80的处理函数。而系统调用的其它参数arg1, arg2, …则依次被放入ebx, ecx, . . .等通用寄存器中,并作为系统调用处理函数的参数,这些参数是怎样传入核心的将会在后面介绍。

下面将示例说明:
int func1()
{
int fd, retval;
fd = open(filename, ……);
……
ioctl(fd, cmd, arg);
. . .
}

func2()
{
int fd, retval;
fd = open(filename, ……);
……
__asm__ __volatile__(\
"int $0x80\n\t"\
:"=a"(retval)\
:"0"(__NR_ioctl),\
"b"(fd),\
"c"(cmd),\
"d"(arg));
}
这两个函数在Linux/x86上运行的结果应该是一样的。
若干个库函数可以映射到同一个系统调用入口点。系统调用入口点对每个系统调用定义其真正的语法和语义,但库函数通常提供一个更方便的接口。如系统调用exec有集中不同的调用方式:execl, execle,等,它们实际上只是同一系统调用的不同接口而已。对于这些调用,它们的库函数对它们各自的参数加以处理,来实现各自的特点,但是最终都被映射到同一个核心入口点。

2. 系统调用陷入内核后作的参数传递过程
当进程执行系统调用时,先调用系统调用库中定义某个函数,该函数通常被展开成前面提到的_syscallN的形式通过INT 0x80来陷入核心,其参数也将被通过寄存器传往核心。
在这一部分,我们将介绍INT 0x80的处理函数system_call。
思考一下就会发现,在调用前和调用后执行态完全不相同:前者是

在用户栈上执行用户态程序,后者在核心栈上执行核心态代码。那么,为了保证在核心内部执行完系统调用后能够返回调用点继续执行用户代码,必须在进入核心态时保存时往核心中压入一个上下文层;在从核心返回时会弹出一个上下文层,这样用户进程就可以继续运行。
那么,这些上下文信息是怎样被保存的,被保存的又是那些上下文信息呢?这里仍以x86为例说明。
在执行INT指令时,实际完成了以下几条操作:
(1) 由于INT指令发生了不同优先级之间的控制转移,所以首先从TSS(任务状态段)中获取高优先级的核心堆栈信息(SS和ESP);
(2) 把低优先级堆栈信息(SS和ESP)保留到高优先级堆栈(即核心栈)中;
(3) 把EFLAGS,外层CS,EIP推入高优先级堆栈(核心栈)中。
(4) 通过IDT加载CS,EIP(控制转移至中断处理函数)
然后就进入了中断0x80的处理函数system_call了,在该函数中首先使用了一个宏SAVE_ALL,该宏的定义如下所示:
#define SAVE_ALL \
cld; \
pushl %es; \
pushl %ds; \
pushl %eax; \
pushl %ebp; \
pushl %edi; \
pushl %esi; \
pushl %edx; \
pushl %ecx; \
pushl %ebx; \
movl $(__KERNEL_DS),%edx; \
movl %edx,%ds; \
movl %edx,%es;
该宏的功能一方面是将寄存器上下文压入到核心栈中,对于系统调用,同时也是系统调用参数的传入过程,因为在不同特权级之间控制转换时,INT指令不同于CALL指令,它不会将外层堆栈的参数自动拷贝到内层堆栈中。所以在调用系统调用时,必须先象前面的例子里提到的那样,把参数指定到各个寄存器中,然后在陷入核心之后使用SAVE_ALL把这些保存在寄存器中的参数依次压入核心栈,这样核心才能使用用户传入的参数。

下面给出system_call的源代码:
ENTRY(system_call)
pushl %eax # save orig_eax
SAVE_ALL
GET_CURRENT(%ebx)
cmpl $(NR_syscalls),%eax
jae badsys
testb $0x20,flags(%ebx) # PF_TRACESYS
jne tracesys
call *SYMBOL_NAME(sys_call_table)(,%eax,4)
. . . . . .
在这里所做的所有工作是:
Ⅰ.保存EAX寄存器,因为在SAVE_ALL中保存的EAX寄存器会被调用的返回值所覆盖;
Ⅱ.调用SAVE_ALL保存寄存器上下文;
Ⅲ.判断当前调用是否是合法系统调用(EAX是系统调用号,它应该小于NR_syscalls);
Ⅳ.如果设置了PF_TRACESYS标志,则跳转到syscall_trace,在那里将会把当前程挂起并向其父进程发送SIGTRAP,这主要是为了设置调试断点而设计的;
Ⅴ.如果没有设置PF_TRACESYS标志,则跳转到该系统调用的处理函数入口。这里是以EAX(即前面提到的系统调用号)作为偏移,在系统调用表sys_call_table中查找处理函数入口地址,并跳转到该入口地址。

(补充说明:
1.GET_CURRENT


#define GET_CURRENT(reg) \
movl %esp, reg; \
andl $-8192, reg;
其作用是取得当前进程的task_struct结构的指针返回到reg中,因为在Linux中核心栈的位置是task_struct之后的两个页面处(8192bytes),所以此处把栈指针与-8192则得到的是task_struct结构指针,而task_struct中偏移为4的位置是成员flags,在这里指令testb $0x20,flags(%ebx)检测的就是task_struct->flags。)



INT 0x80 SS

pt_regs
ESP


SALL_ALL 用户栈




CALL syscall_entry



核心栈

堆栈中的参数分析:

正如前面提到的,SAVE_ALL是系统调用参数的传入过程,当执行完SAVE_ALL并且再由CALL指令调用其处理函数时,堆栈的结构应该如上图所示。这时的堆栈结构看起来和执行一个普通带参数的函数调用是一样的,参数在堆栈中对应的顺序是(arg1, ebx),(arg2, ecx),(arg3, edx). . . . . .,这正是SAVE_ALL压栈的反顺序,这些参数正是用户在使用系统调用时试图传送给核心的参数。下面是在核心的调用处理函数中使用参数的两种典型方法:
asmlinkage int sys_fork(struct pt_regs regs);
asmlinkage int sys_open(const char * filename, int flags, int mode);
在sys_fork中,把整个堆栈中的内容视为一个struct pt_regs类型的参数,该参数的结构和堆栈的结构是一致的,所以可以使用堆栈中的全部信息。而在sys_open中参数filename, flags, mode正好对应与堆栈中的ebx, ecx, edx的位置,而这些寄存器正是用户在通过C库调用系统调用时给这些参数指定的寄存器。
__asm__ __volatile__(\
"int $0x80\n\t"\
:"=a"(retval)\
:"0"(__NR_open),\
"b"(filename),\
"c"(flags),\
"d"(mode));


核心如何使用用户空间的参数:

在使用系统调用时,有些参数是指针,这些指针所指向的是用户空间DS寄存器的段选择子所描述段中的地址,而在2.2之前的版本中,核心态的DS段寄存器的中的段选择子和用户态的段选择子描述的段地址不同(前者为0xC0000000, 后者为0x00000000),这样在使用这些参数时就不能读取到正确的位置。所以需要通过特殊的核心函数(如:memcpy_fromfs, mencpy_tofs)来从用户空间数据段读取参数,在这些函数中,是使用FS寄存器来作为读取参数的段寄存器的,FS寄存器在系统调用进入核心态时被设成了USER_DS(DS被设成了KERNEL_DS)。在2.2之后的版本用户态和核心态使用的DS中段选择子描述的段地址是一样的(都是0x00000000),所以不需要再经过上面那样烦琐的过程而直接使用参数了。

内存映射分析:

Linux将4G的地址划分为用户空间和内核空间两部分。在Linux内核的低版本中(2。0。X),通常0-3G为用户空间,3G-4G为内核空间。这个分界点是可以可以改动
的。
正是这个分界点的存在

,限制了Linux可用的最大内存为2G.而且要通过重编内核,调整这个分界点才能达到。实际上还可以有更好的方法来解决这个问题。由于内核空间与用户空间互不重合,所以可以用段机制提供的保护功能来保护内核级代码。

以下为2.0.X的部分代码:
/usr/src/linux/arch/i386/kernel/entry.S
A: .quad 0xc0c39a000000ffff /* 0x10 kernel 1GB code at 0xC0000000 *
B: .quad 0xc0c392000000ffff /* 0x18 kernel 1GB data at 0xC0000000 *
C: .quad 0x00cbfa000000ffff /* 0x23 user 3GB code at 0x00000000 *
D: .quad 0x00cbf2000000ffff /* 0x2b user 3GB data at 0x00000000 *

A,B为内核代码段及数据段的描述符。C,D为用户代码及数据段的描述符
从以上,我们可以清楚的看到A,B的特权级为0,而C,D的特权级为3。当内核
存取用户空间的内容时,他借助于fs寄存器,同过将FS寄存器的内容置为D
来达到访问用户空间的目的。

2.2.X版的 内核对此进行了改动。这样内核空间扩张到了4G,所以可以直接进行拷贝了

.quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 *
.quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 *
.quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 *
.quad 0x00cff2000000ffff /* 0x2b user 4GB data at 0x00000000 *

从表面上看内核的基地址变为了0,但实际上,内核通常仍在虚址3G以上。其中奥妙在与 不同的连接描述文件:
2.2.X: = 0xC0000000 + 0x100000;
2.0.X:= 0x100000;

2.0.X的起址为0x100000。这样一来,二者就相等了。
都是0xC0000000 + 0x100000

用户空间在2.2.X中从直观上变为0-4G,让人迷惑:其不是可以直接访问内核了?
其实不然, 同过使用页机制提供的保护,阻止了用户程序访问内核空间。

这样,存取用户空间实际上已不需要FS,GS的支持。但在内核中仍保留set_fs(X)等宏上你设的值用来验证随后的操作是否合适。是否超过设定的X。此处X不再是一个段描述符,而是一个具体的值。
此处就有一个陷阱:如果你将Set_fs的值设置为Kernel_DS,而没有将其该回去,当用户通过系统调用将一个Buffer的地址(应该在用户空间)设置为一个内核空间,而内核在访问该地址前认为默认当前的阀值仍为User_DS,事情就大大?

相关主题
文本预览
相关文档 最新文档