http://www.linuxsir.org/bbs/showthread.php?t=238230
Linux1.0核心游记下载地址:
http://www.linuxabc.net/book/bookinfo.php?id=362
http://www.prowork.com.cn/manual/upload/Linux1.0%E6%A0%B8%E5%BF%83%E6%B8%B8%E8%AE%B0
静态及动态添加系统调用
――――――――摘之 “Linux1.0核心游记”
A2.系统调用的添加
A2-1静态添加系统调用
所谓的静态静态添加系统调用,是指我们直接通过修改核心的源代码而达到的。只要我们知道Linux下系统调用实现的框架,添加(当然也可以修改)系统调用将会是件非常简单的事情。
该方法的缺点还是有的:
1. 修改好源代码后需要重新编译核心,这是个非常长和容易发生错误的过程。
2. 对于你修改及编译好后所得到的核心,你所做的添加(修改)是静态的,无法在运行时动态改变(所以也就有了下面的动态方法)
A2-1-1讨论Linux系统调用的体系
在Linux的核心中,0x80号中断是所有系统调用的入口(当然你也可以修改,因为我们有源代码吗 :),不过你改了之后只能自己玩玩,要不然人家0x80号中断的程序怎么执行呢?)。但是还是有办法(可能还有其他办法)。办法是在你看了下面的“动态添加系统调用”后就知道,这个就留给读者考虑了。
用0x80中断号功能作为所有的系统调用入口点,是系统编写者定义的(也可以说是Linus定义的)。下面我们看一下其设置的代码(取之2.4核心,我们只看x386)
定义于Arch/i386/kernel/traps.c(很简单,就一个函数调用)
set_system_gate(SYSCALL_VECTOR,&system_call);!设置0x80号中断SYSCALL_VECTOR默认是0x80(你可以修改)
system_call定义在Arch\i386\kernel\entry.S
set_system_gate定义在Arch/i386/kernel/traps.c,具体的代码分析这里就不做介绍了。大致的功能是把system_call的地址(当然还有其他内容,比如类型值及特权级)设置到IDT(中断描述符表)的第0x80项中(请注意每项是8个字节,在基础有所介绍)。
当用了set_system_gate设置好中断号,并且已经开中断。接下来我们就可以用编程的方式来调用该中断号。调用中断的汇编指令是“int”。
CH3-5/hello.c
!同时请注意在该程序中我们也用了strlen函数,它是C库中定义的标准函数
!不过,这里我们只需关注代码中的汇编代码即可。
#include
#include
int
main()
{
int value = -1;
char *lpBuffer = "Hello everybody.\n";
unsigned long sys_num = 4;
int iLen = strlen(lpBuffer);
__asm__("int $0x80":"=a"(value) //输出值(即printf执行后的返回值)
:"0"((long)(sys_num)), //eax=sys_num=4,sys_write的系统调用号
"b"(1), //参数一:文件描述符(stdout)
"c"(lpBuffer), //参数二:要显示的字符串
"d"(iLen)); //参数三:字符串长度
return value;
}
CH3-5/Makefile
GCC=gcc
OBJS=hello.o
.c.o:
$(GCC) -c $<
all:$(OBJS)
$(GCC) $(OBJS) -o hello
clean:
rm -f *.o core
clobber:clean
rm -f hello
这里的代码编译后,我们便可以执行了。其输出结果就如我们调用标准C库中的printf函数一样。请看下图
我们更关心的是系统调用的实现机制。下面请跟我来看吧。
1. __asm__("int $0x80"2. :"=a"(value) //输出值(即sys_write执行后的返回值)
3. :"0"((long)(sys_num)), //eax=sys_num=4,sys_write的系统调用号
4. "b"(1), //参数一:文件描述符(stdout)
5. "c"(lpBuffer), //参数二:要显示的字符串
6. "d"(iLen)); //参数三:字符串长度
第1句代码用于执行0x80号中断。当程序执行到这句时,CPU会从用户态切换进核心态(也就是我们通常说的ring0级),并且同时会把ss,esp,eflags,cs,eip按顺序入栈。
从第2句到第6句代码,用于把系统调用号及参数一到到参数三设置到对应的寄存器中。eax=sys_num(系统调用号)
ebx=1(参数一,标准输出)
ecx=lpBuffer的值(参数二,要显示的字符串)
edx=iLen(参数三,字符串长度)
接下来执行system_call函数(所以我们也说该函数是所有系统调用的入口点函数)。于是我们接着看system_call函数。
ENTRY(system_call)
1. pushl %eax # save orig_eax
2. SAVE_ALL
3. ……4. cmpl $(NR_syscalls),%eax
5. jae badsys
6. call *SYMBOL_NAME(sys_call_table)(,%eax,4)
7. ……8. restore_all:
9. RESTORE_ALL
第1句代码首先把eax值入栈,我们可以知道该eax中保存的就是我们从用户态传入的系统调用号(从代码的注释也可以看出 :))。
第2句代码是个宏定义,定义如下:
#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;
从宏的代码中,可以得知它把CPU中的一些寄存器值入栈(为了从核心态返回时还能回到进入前(用户态)的状态),同时还把核心态的数据段值写入ds,es(这样的话,我们就可以访问核心的态的数据段了,代码段在执行int指令时已经由CPU自动设置了)。
第4,5句代码是在测试我们传入的系统调用号是否超过了当前系统所支持的最大系统调用数(对于2.4核心,支持的最大系统调用数是260个,当然你可以修改)。
第6句代码用传入的系统调用号,查表获得对应的系统调用函数地址并call之。该表定义如下(定义在Arch/i386/kernel/entry.S):
.data
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_ni_syscall) /* 0 - old "setup()" system call*/
.long SYMBOL_NAME(sys_exit)
.long SYMBOL_NAME(sys_fork)
.long SYMBOL_NAME(sys_read)
.long SYMBOL_NAME(sys_write).long SYMBOL_NAME(sys_open) /* 5 */
……
我们给出的例子程序调用号是4,所以根据上表我们可以知道对应的系统调用函数是sys_write。这样我们就进入了真正的系统调用处理函数了。(关于sys_write实现这里不做介绍)
从第7句向后的代码,便是系统调用执行完成后的善后处理工作。这里我们给出上面的SAVE_ALL宏的相反操作。即RESTORE_ALL,代码如下:
#define RESTORE_ALL \popl %ebx; \
popl %ecx; \
popl %edx; \
popl %esi; \
popl %edi; \
popl %ebp; \
popl %eax; \
1: popl %ds; \
2: popl %es; \
addl $4,%esp; \
3: iret; \
.section .fixup,"ax"; \
4: movl $0,(%esp); \
jmp 1b; \
5: movl $0,(%esp); \
jmp 2b; \
6: pushl %ss; \
popl %ds; \
pushl %ss; \
popl %es; \
pushl $11; \
call do_exit; \
.previous; \
.section __ex_table,"a";\
.align 4; \
.long 1b,4b; \
.long 2b,5b; \
.long 3b,6b; \
.previous
从宏的代码中,可以得知它把CPU中的一些寄存器值出栈。并且执行iret指令返回到用户态。到这为止,系统调用便执行完成了,即实现了我们所需要的功能。最后我们用一张简单的图来描述之。请看下图(该图为了描述方便,没有把情况都描述清楚,比如系统调用号超过系统定义的最大系统调用数等等):
A2-1-2修改代码来添加系统调用
通过上面的介绍,我们可以知道修改系统调用并不是件难事。那么我们就开始修改吧。(假定你的核心在/usr/src/linux下)
第1步:
我们打开include/linux/sys.h文件,修改
#define NR_syscalls 260
为
#define NR_syscalls 261(假设我们只要添加一个系统调用)
第2步:
个人认为这一步,也可以不做,因为作为系统调用的添加者,你当然是知道你加的系统调用号的。不过我们还是不忽略它。请打开include/asm-i386/unistd.h添加如下代码
#define __NR_helloworld 259
这个名字,你可以自己决定用什么,只要不和系统冲突。
第3步:
打开arch/i386/kernel/entry.S,在 .long SYMBOL_NAME(sys_set_tid_address)的后面加入你要添加的系统调用函数名。假设我们要添加的函数名是sys_helloworld,于是我们写成这样:
.long SYMBOL_NAME(sys_helloworld)
第4步:
在第3步我们只是添加了系统调用的声明,还要添加系统调用的实现体才行(关于系统调用实现体的添加,有两个方法:第一个方法是写在系统核心的某个文件中,第二个方法是在核心中新添加一个文件,不过用该方法你需要修改对应的Makefile文件。这里我们采用第一个方法。)。
对于本例,我们把实现体写在fs/read_write.c中。添加代码如下:
asmlinkage void sys_helloworld (void)
{
printk(“Hello world.\n”);
}
第4步:
编译核心。
第5步:
核心编译成功后,我们便可以编写代码测试了。这里我们就修改上面的CH3-5/hello.c代码就可以的。Makefile不变。
修改的代码如下:
CH3-5/hello.c
#include
#include
int
main()
{
unsigned long sys_num = 259;
__asm__("int $0x80"::"a"((long)(sys_num))); //系统调用号
return 0;
}
修改好后,我们就可以编译并执行了。执行情况如下:
到这为止,静态添加系统调用便完成了。
A2-2动态添加系统调用
所谓动态添加系统调用,就是在Linux运行的时候把新的系统调用加入。从而避免了编译核心的问题。
A2-2-1动态添加系统调用的原理
动态添加系统可能会有很多种方法,这里我们只讨论一种方法。个人认为本书讨论的这种方法是比较好的。同样你也要具有超级用户的权限。
通过上面的对1.0的代码的分析,我们可以知道0x80中断号是整个系统调用的入口点。进入该入口点后,通过比较传入各种系统调用号来查表得到对应的系统调用处理函数。所以这里我们要是能够取得中断描述符表的基地址,然后以此为起点计算出0x80项的地址值。在从该地址处中分解出系统调用的入口点,然后用我们自己定义的入口点替代它。在我们定义的入口点函数中处理我们要处理的系统调用号(随便你干什么)。处理完后在扔给被我们替代掉的入口点函数,即可!下面还是用图来表示,我怕写的不明白!
通过上图我们可以知道,idt表是个什么样子,并且其中每项的内容。那么谁来描述idt表呢?即谁来定位idt表呢?在X86 CPU上有两条指令来和其相关,它们是“sidt”和“lidt”,分别用来复制、加载idt。请注意这两条指令的操作数均为48位。我们可以定义如下结构来描述之:
struct idt_48 {
unsigned short limit; !描述idt表的大小
unsigned long base; !idt表的基地址,看到了吧这就是基地址了
};
所以,我们可以用sidt指令来获取系统idt表的48位值,然后从中获取基地址,再根据取得的基地址计算得到第0x80项中的值,取得第0x80项的值后,我们便可以根据上图中定义的结构分解来得到系统调用的入口点函数地址。
啊!等等!如果我把这个地址修改了,不就实现了动态添加、修改系统调用了吗?
恭喜你!你说对了,下面我们可以看代码了。
A2-2-2实现动态添加、修改系统调用
CH3-6/capturemod.c
#ifndef MODULE
#define MODULE
#endif
#ifndef __KERNEL__
#define __KERNEL__
#endif
#ifndef NULL
#define NULL 0L
#endif
#include
#include
#include
#include
MODULE_LICENSE("GPL");
//请查看模块的编写那节有描述
void new80_handle();
//新的0x80处理句柄
static unsigned long old80_handle;
//用于保存老的0x80处理句柄
extern char * getname(const char * filename);
//取的用户空间的程序名extern kmem_cache_t *names_cachep;
//用于释放内存
static unsigned long eax, ebx,ecx;
//保存寄存器内容
struct descriptor_idt
{
unsigned short offset_low;
unsigned short ignore1;
unsigned short ignore2;
unsigned short offset_high;
};
//用于描述idt表中的一项,共8个字节
static struct {
unsigned short limit;
unsigned long base;
}__attribute__ ((packed)) idt48;
//用于定位idt表
static void puppet_handle(void)
{
//首先一点要明确的是,我们不是替代系统中所有系统//调用,而只是在原来的基础上修改或者新增系统调用.
//对原来的没有被你修改的系统调用不能有任何的影响.
//所以要保证堆栈的正确.
//看到这个函数名(puppet_handle),你可能会感到这是个伪函数。
//没错,确实是的,该函数没有什么大的作用,只是用来包裹下面的
//一段嵌入汇编.代码.
//因为在*.c文件中,你不能直接写汇编代码,
//所以我们只有将其写成嵌入汇编的形式了.
//现在我们讨论一下为什么要写用嵌入汇编.
//我们先回忆一下发生系统调用时的堆栈的情况
//系统调用发生时,CPU会按顺序将SS,ESP,EFLAGS,CS,EIP
//这几个寄存器压入堆栈.,然后会调用” system_call”函数(也就
//是系统调用的总入口点函数),在该函数中会按顺序压入如下
//寄存器值ORIG_EAX,ES,DS,EAX,EBP,EDI,ESI,EDX,ECX,EBX(这
//里压入的寄存器值其实就是系统调用函数将会用的各个参数).通过这里
//的描述,我们可以知道如果” new80_handle”,用C语言来写的是不可能
//完成的.因为你要在调用” new80_handle”前把原来在system_call中压入的
//寄存器内容先压入堆栈,并且还要修改从new80_handle返回地址为原系统的
//总的入口点函数.可是系统的核心原来已经是编译好的了,总不能让你去修改
//编译好的二进制文件吧.
//所以,我们用汇编代码绕过去,并且也不需要知道核心的入栈顺序了.从而保证
//了在进入老的系统调用总的入口点前堆栈是正确的.
//而在该汇编代码中调用另一个辅助的函数,//在该辅助的函数中完成要做的所有工作
//这里还有特别的一点说明是,为了不污染正在运行核心的函数命名空间
//(因为模块被安装后,其全局变量或者函数会到处),所以,所有的函数和变量
//被我们用了static来做修饰.也许读者对void new80_handle();这个函数有疑问
//因为我们没有用static来做修饰啊!难道它不会污染命名空间吗,会的.//不过,我们在写嵌入汇编代码时,用了".type new80_handle,@function\n"
//做了修饰,这样的话new80_handle就如static类型的函数一样了//不要担心,在下面我们会讨论这个用C语言编写的模块的
//反汇编代码的.你会更加明白的.
__asm__ (
".type new80_handle,@function\n" //用于修饰函数,以让其和用static修饰的一样".align 4\n" //内存对齐方式
"new80_handle: \n" //new80_handle入口点
"pusha \n" //所有通用寄存器入栈
"pushl %%es\n" //段寄存器fs入栈
"pushl %%ds\n" //段寄存器ds入栈
"movl %%eax,%0\n""movl %%ebx,%1\n"
"movl %%ecx,%2\n" //取出我们要用的寄存器值
"call real_handler \n" //调用real_handle,正如其名.在该函数中//可以完成我们想做的任何事情
"popl %%ds\n" //段寄存器ds出栈
"popl %%es\n" //段寄存器fs出栈
"popa \n" //所有通用寄存器出栈
"jmp *old80_handle" //我们的工作完成后,调用系统起先的//系统调用入口点,一来可以让它处理
//我们没有做的事情(比如从核心态返回到
//用户态).二来如果不是我们添加的系统调用
//它还能继续处理
::"m"(eax),"m"(ebx),"m"(ecx)
);
}
static void real_handler()
{
char *pName = NULL; //指向拷贝到核心中程序名
if(eax == __NR_execve)
{//捕获sys_execve调用pName = getname((char*)ebx); //getname用于把用户态的数据拷贝到
//核心态,ebx中保存了sys_execve系统调用
//的第一个参数,也就是所要执行文件的文件名
//的地址
if(pName)
{
printk(KERN_INFO"The program is %s.\n",pName);
//打印提示信息kmem_cache_free(names_cachep, (void *)( pName));
//释放在getname中所分配的内存}
}
else if(eax == 0x200)
{ //截获0x200号系统调用,该系统调用不存在,用我给的程序测试!//假设这里只打印eax,ebx,ecx,当然你也可以做其他事情!
printk(KERN_INFO"eax=0x%x, ebx=0x%x, ecx=0x%x\n",eax,ebx,ecx);
}
}
int init_module(void)
{
__asm__ volatile ("sidt %0": "=m" (idt48));
//取得idt表的48位值struct descriptor_idt *pIdt80 = (struct descriptor_idt *)(idt48.base + 8*0x80);
//并让pIdt80指向idt表中第0x80项old80_handle = (pIdt80->offset_high<<16 | pIdt80->offset_low);
//保存老的总入口点unsigned long new80_addr = (unsigned long)new80_handle;
//把新的0x80入口点函数地址转换成unsigned long,为了便于下面可以分解pIdt80->offset_low = (unsigned short) (new80_addr & 0x0000FFFF);
//把新入口点的低16位设置到0x80项中对应处pIdt80->offset_high = (unsigned short) (new80_addr >> 16);
//把新入口点的高16位设置到0x80项中对应处//另外要注意的是,我们没有改变ignore1和ignore2的内容.如果你很想修改这两个字
//段的内容,请确认你知道在做什么,这里不讨论.
printk(KERN_INFO"Ok,we capture syscall successful.\n");
//打印提示信息return 0;
}
void cleanup_module()
{
__asm__ volatile ("sidt %0": "=m" (idt48));
//取得idt表的48位值struct descriptor_idt *pIdt80 = (struct descriptor_idt *)(idt48.base + 8*0x80);
//并让pIdt80指向idt表中第0x80项pIdt80->offset_low = (unsigned short) (old80_handle & 0x0000FFFF);
//恢复老的入口点的低16位pIdt80->offset_high = (unsigned short) (old80_handle >> 16);
//恢复老的入口点的高16位printk(KERN_INFO"Ok,we leave capture.\n");
//打印提示信息}
CH3-6/Makefile
GCC=gcc
KERNELDIR=/usr/src/linux/include
OBJS=capturemod.o
TESTMOD=testmod/testmod
.c.o:
$(GCC) -D__KERNEL__ -I$(KERNELDIR) -c $^ -o $@
all:$(OBJS) $(TESTMOD)
$(TESTMOD):
make -C testmod
insert:$(OBJS)
/sbin/insmod $(OBJS)
remove:
/sbin/rmmod helloworld
clean:
rm -f *.o core
rm -f testmod/*.o
clobber:clean
rm -f $(TESTMOD)
CH3-6/ testmod/ testmod.c(用于测试上面的模块程序)
#include
#include
int
main()
{
int sys_num = 0x200;//512号系统调用(不存在,用来测试上面的模块而已)
long value = 0;
__asm__("int $0x80"
:"=a"(value)
:"0"((int)sys_num));
printf("The value is %d.\n",value);
return value;
}
CH3-6/testmod/ Makefile
GCC=gcc
OBJS=testmod.o
.c.o:
$(GCC) -c $<
all:$(OBJS)
$(GCC) $(OBJS) -o testmod
clean:
rm -f *.o core
clobber:clean
rm -f testmod
下面请看编译和执行情况,这里直给出截图,正所谓一图千言吗!
下图是编译的情况
下图是我们插入模块时的情况
下图是测试程序执行的情况,请读者仔细看屏幕的提示,我不做解释了
下图是模块注销的情况
A2-2-3 反汇编capturemod.o并分析之
本节来做capturemod.o的反汇编代码的分析,这样我们能够更好的理解上面的C语言所完成的代码.通过objdump命令可以反汇编上面的capturemod.o模块.
请在命令行下输入如下命令”objdump –D capturemod.o > mod.s”打开后便是下面的内容.请跟着我看吧
capturemod.o: file format elf32-i386
//识别出该模块格式是elfDisassembly of section .text:
//代码段的反汇编00000000
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 90 nop
//上面的3句代码,是puppet_handle反汇编代码,什么也不做,并且没有任何地方调用它00000004
4: 60 pusha //所有通常寄存器值内容入栈
5: 06 push %es //段寄存器es 入栈
6: 1e push %ds //段寄存器ds 入栈
7: a3 04 00 00 00 mov %eax,0x4c: 89 1d 08 00 00 00 mov %ebx,0x8
12: 89 0d 0c 00 00 00 mov %ecx,0xc //把eax,ebx,ecx放入0x4,0x8,0xc处(看到//这个样子大家可能会有点奇怪,为什么
//地址这么小呢?原因在于在该模块被
//插入核心时,才会被重定位)
18: e8 0b 00 00 00 call 28
1d: 1f pop %ds
1e: 07 pop %es
1f: 61 popa //把上面入栈的内容退回到对应的寄//存器中,这样我们就保证了堆栈的
//正确行,让真正的系统调用处理
//函数感觉不到我们已经做过了
//我们需要做的事
20: ff 25 00 00 00 00 jmp *0x0 //直接跳转到老的系统调用入口处
26: c9 leave //下面这两句代码是不可能被执行的
27: c3 ret //因为跳到老的系统调用入口点时用的
//是jmp指令00000028
//分析了,因为它会随着你的不同实现
//而不同
28: 55 push %ebp
29: 89 e5 mov %esp,%ebp
2b: 83 ec 08 sub $0x8,%esp
2e: c7 45 fc 00 00 00 00 movl $0x0,0xfffffffc(%ebp)
35: 83 3d 04 00 00 00 0b cmpl $0xb,0x4
3c: 75 43 jne 81
3e: 83 ec 0c sub $0xc,%esp
41: ff 35 08 00 00 00 pushl 0x8
47: e8 fc ff ff ff call 48
4c: 83 c4 10 add $0x10,%esp
4f: 89 45 fc mov %eax,0xfffffffc(%ebp)
52: 83 7d fc 00 cmpl $0x0,0xfffffffc(%ebp)
56: 74 54 je ac
58: 83 ec 08 sub $0x8,%esp
5b: ff 75 fc pushl 0xfffffffc(%ebp)
5e: 68 00 00 00 00 push $0x0
63: e8 fc ff ff ff call 64
68: 83 c4 10 add $0x10,%esp
6b: 83 ec 08 sub $0x8,%esp
6e: ff 75 fc pushl 0xfffffffc(%ebp)
71: ff 35 00 00 00 00 pushl 0x0
77: e8 fc ff ff ff call 78
7c: 83 c4 10 add $0x10,%esp
7f: eb 2b jmp ac
81: 81 3d 04 00 00 00 00 cmpl $0x200,0x4
88: 02 00 00
8b: 75 1f jne ac
8d: ff 35 0c 00 00 00 pushl 0xc
93: ff 35 08 00 00 00 pushl 0x8
99: ff 35 04 00 00 00 pushl 0x4
9f: 68 20 00 00 00 push $0x20
a4: e8 fc ff ff ff call a5
a9: 83 c4 10 add $0x10,%esp
ac: c9 leave
ad: c3 ret
000000ae
ae: 55 push %ebp
af: 89 e5 mov %esp,%ebp
b1: 83 ec 08 sub $0x8,%esp//在栈上留下空间,该空间是为pIdt80和new80_addr
//留的
b4: 0f 01 0d 10 00 00 00 sidtl 0x10 //取得idt的48位指针
bb: a1 12 00 00 00 mov 0x12,%eax //把后32位值送入eax,也就是idt表
//的基址
c0: 05 00 04 00 00 add $0x400,%eax //取得第0x80项后,放入eax中
c5: 89 45 fc mov %eax,0xfffffffc(%ebp)
c8: 8b 45 fc mov 0xfffffffc(%ebp),%eax
cb: 0f b7 40 06 movzwl 0x6(%eax),%eax //取出第0x80项的第6个字节开始
//的两个字节(高两个字节)cf: 89 c2 mov %eax,%edx//把取出的字节放入edx
d1: c1 e2 10 shl $0x10,%edx //左移16位
d4: 8b 45 fc mov 0xfffffffc(%ebp),%eax
d7: 0f b7 00 movzwl (%eax),%eax //低16位da: 09 d0 or %edx,%eax//相或后便是老的系统入口点函数
dc: a3 00 00 00 00 mov %eax,0x0 //保存起来
e1: c7 45 f8 04 00 00 00 movl $0x4,0xfffffff8(%ebp)
e8: 8b 55 fc mov 0xfffffffc(%ebp),%edx
eb: 8b 45 f8 mov 0xfffffff8(%ebp),%eax
ee: 66 89 02 mov %ax,(%edx)//取的低16位,刚好放在//(edx)的低16位,也就是第
//0-1两个字节中
f1: 8b 55 fc mov 0xfffffffc(%ebp),%edx
f4: 8b 45 f8 mov 0xfffffff8(%ebp),%eax
f7: c1 e8 10 shr $0x10,%eax //高16位
fa: 66 89 42 06 mov %ax,0x6(%edx)//从高6个字节处开始放//到此时,也就意味着
//修改了老的入口点了
fe: 83 ec 0c sub $0xc,%esp
101: 68 60 00 00 00 push $0x60 106: e8 fc ff ff ff call 107
10b: 83 c4 10 add $0x10,%esp
10e: b8 00 00 00 00 mov $0x0,%eax
113: c9 &