对main函数返汇编后出现两个ret的研究
文章作者:星星铁
研究的主题:只有一个main函数的c程序,反汇编后发现后面有两个ret指令,那么最后面一个ret指令有什么作用呢?
*******************************************************************
问题重现。
程序1.c:
main()
{
_AX = 0;
return;
}
编译—>连接—>用debug加载后在1fa处查看前四条汇编代码是:
-u1fa
0CC2:01FA 33C0 XOR AX,AX
0CC2:01FC EB00 JMP 01FE
0CC2:01FE C3 RET
0CC2:01FF C3 RET
前面三条大家都很清楚:
“XOR AX,AX” 用异或指令(xor 相异为真,相同为假)将AX寄存器里面的值置成0。
“JMP 01FE” 跳到ret指令处,此处jmp貌似没用可以省略,其实很重要,不能省略的原因是:一个函数中可能有多条return这样的c代码,但是只有一条ret指令,return指令都会转化为一条跳转到ret指令的jmp指令。
第一个“ret”指令 子程序返回作用。
第二个“ret”指令 这个ret指令有什么作用呢?初次看到这个ret指令很容易猜想,这个ret指令是不是main函数的一部分,如果是main函数一部分那就什么都好说了,main函数自带了一个永远也用不到的ret指令,那只有问问写编译器的人怎么想的了。
****************************************************
验证不知有何作用的ret指令是否属于main函数一部分。
根据实践经验很容易发现,这个ret指令不属于main函数的指令。
例如程序2.c:
main()
{
_AX = 0;
return;
}
fun()
{
_BX = 0;
return;
}
同样用debug加载后得到如下汇编代码。
-u1fa
0CC2:01FA 33C0 XOR AX,AX main函数
0CC2:01FC EB00 JMP 01FE
0CC2:01FE C3 RET
0CC2:01FF 33DB XOR BX,BX fun函数
0CC2:0201 EB00 JMP 0203
0CC2:0203 C3 RET
0CC2:0204 C3 RET 不知道用处的ret指令
可以看到,main函数只占前三条指令,后面紧接着的是fun函数的三条指令,那个不知有何用途的ret指令跑到fun函数后面去了。所以结论是不知有何用途的ret指令不属main函数一部分,我们的问题没有解决。
但是这一步里我们把程序改造一下其实可以得到一个很有用的信息;
****************************************************
得到有用信息。
例如程序3.c
main()
{
_AX = 0;
return;
}
fun()
{
}
将2.c程序改造成这个样子,发现汇编代码是:
-u1fa
0CC2:01FA 33C0 XOR AX,AX main函数
0CC2:01FC EB00 JMP 01FE
0CC2:01FE C3 RET
0CC2:01FF C3 RET fun函数
0CC2:0200 C3 RET 不知道用处的ret指令
发现我们的fun函数反汇编后只有一条ret指令,这一条ret指令也就足以构成一个子程序了。那么最后一条不知道用途的ret指令可以很容易想到它也是由一个函数转化而来,我们将这个函数取名为X。只是这个X函数没有实质性内容。这里我们得到的很有用的信息是这个ret指令是一个函数。但是这个函数唯一留给我们的信息太少了只有一个ret指令,情况陷入僵局。我们知道一个函数的名字很容易联想到他的作用,比如printf格式化打印,puts打印字符串。如果我们这个X函数的函数名,那么说不定能得到一些线索。
****************************************************
得到X函数的名字。
我们分析一下:我们拿到X函数只有一个ret指令,一旦被连接完成,exe文件里面不存在各个函数的函数名。所以我们不能再寄希望于exe文件了。下面用到的一些自己研究到的一些结论作为研究依据。
tlink 使用的语法是:Syntax: TLINK objfiles, exefile, mapfile, libfiles
这个大家都用过了,其中objfiles,exefile,libfiles大家都知道什么意思。这个mapfile其实也很有用,这个文件记录了我们所生成的exe文件的详细信息,包括有哪些段以及各个段名和各个起始地址、有哪些函数和各个函数的起始地址,有哪些全局变量以及起始地址。
我们编写如下程序4.c
main()
{}
然后只用tcc.exe进行编译:tcc –c 4.c
然后用tlink生成exe文件及我们所需要的map文件:”tlink c0s 4,4,4,cs /s” 用edit打开所生成的4.map文件我们可以看到很多信息,完整的信息内容我放到一楼。这里要告诉大家的是,有的同学想用汇编语言和c语言搭配编程,一般的都只会在c语言中嵌入汇编代码来完成。其实还有一种很好的方法,就是:编写汇编程序,用masm.exe汇编编译器生成obj文件,然后直接与用tcc.exe生成的obj文件进行连接生成exe文件。想这么办的同学不理解map文件中的内容是很难做到的。
Map文件大致分为四个部分:
第一部分:按功能划分的各个段的段信息。如00000H 00555H 00556H _TEXT CODE
第二部分:按模块划分的各个模块的信息:如0000:0000 01FA C=CODE S=_TEXT G=(none) M=C0S ACBP=28
第三部分:以名称为排序关键字的各个全局函数名和全局变量的信息:如0000:01F8 DGROUP@
第四部分:以地址为排序关键字的各个全局函数名和全局变量的信息:如0000:0121 __EXIT
通过第二/三/四部分我们很容易看到我们的main函数起始地址在1fa处,大家看到了么? 而我们的main函数只有一条汇编指令ret,那么那个X函数就应该在1fb处。
在第四部分我们看到这两句:
0000:01FA _MAIN
0000:01FC _EXIT
我们的main函数的确在1fa处但是紧接着的是exit函数它在1fc处,我们看不到1fb处的信息。通过多次试验发现,看不到1fb处信息的原因是:1fb处存放的是一个私有的隐藏的内部函数,不作为接口给它人调用,而map文件中只存放有公共的函数与变量。
但是我们通过第二部分可以看到:
0000:01FA 0001 C=CODE S=_TEXT G=(none) M=4.C ACBP=28
0000:01FB 0030 C=CODE S=_TEXT G=(none) M=EXIT ACBP=28
这里看到1fb处属于EXIT模块。
我们用tlib.exe提取出cs.lib的list文件,命令是:tlib cs * exit,cs.txt将得到cs.txt文件,文件中exit模块描述信息如下:
EXIT size = 54
__exitbuf __exitfopen
__exitopen _exit
这个模块包含有四个关键字,这些关键字有可能代表一个函数名或者全局变量名,且都能被其他函数调用,而X函数不能被显示调用,那么肯定不能代表这四个关键字的某一个。那么这个X函数到底是什么呢?
我们编写如下程序 5.c:
void exit(int);
void _exitbuf();
void _exitfopen();
void _exitopen();
main()
{
printf("%p\n",_exitbuf);
printf("%p\n",_exitfopen);
printf("%p\n",_exitopen);
printf("%p\n",exit);
}
这个程序打印出各个关键字的地址,这些关键字在程序中都需要有声明,原因是编译器要知道这些关键字是什么类型,并预留空间。连接器运行时直接将搜索各个lib文件,找到了所需的各个关键字的真实存在则生成exe文件,不能找到则报错。
打印后得到如下结果:
0200
0202
0204
026B
然后用debug去看发现200、202、204处均不是一个函数的开始,所以判断他们只是全局变量。而26b处是正常的函数开始,而且在26b的前一个字节26a处能找到X函数转化而来的ret指令。单独拿这个exit函数去实验,发现那个神秘的ret指令总是在exit函数的前一个字节处。
我们用debug加载5.exe,然后用g1fa这样的指令执行到1fa处,然后用d 200命令找到三个全局变量处的内容,内容如下:
-d 200
0DFB:0200 6A 02 6A 02 6A 02 00 00-00 10 00 00 00 00 09 02
发现三个全局变量都被置成了26ah这个十六进制数。这个十六进制数就是我们X函数的开始地址,然后用u 26a查看代码:
-u 26a
0CC2:026A C3 RET X函数
0CC2:026B 55 PUSH BP exit函数
0CC2:026C 8BEC MOV BP,SP
0CC2:026E EB0A JMP 027A
0CC2:0270 8B1E0A02 MOV BX,[020A]
0CC2:0274 D1E3 SHL BX,1
0CC2:0276 FF973204 CALL [BX+0432]
0CC2:027A A10A02 MOV AX,[020A]
0CC2:027D FF0E0A02 DEC WORD PTR [020A]
0CC2:0281 0BC0 OR AX,AX
0CC2:0283 75EB JNZ 0270
0CC2:0285 FF160002 CALL [0200] 将200h处的数据也就是__exitbuf复制给ip,调用200处所存的指针指向的函数
0CC2:0289 FF160202 CALL [0202] 将202h处的数据也就是__exitfopen复制给ip,调用200处所存的指针指向的函数
0CC2:028D FF160402 CALL [0204] 将204h处的数据也就是__exitopen复制给ip,调用200处所存的指针指向的函数
0CC2:0291 FF7604 PUSH [BP+04]
0CC2:0294 E88AFE CALL 0121
0CC2:0297 59 POP CX
0CC2:0298 5D POP BP
0CC2:0299 C3 RET
发现exit函数中有三条跳转指令,这三条跳转指令都跳转到X函数处。
我们可以做这样的分析:X函数只有一条指令,这是exit模块(包括三个全局变量和一系列二进制数据)的制作者知道的,而exit函数(只是一个函数)中包含有重复调用X函数的代码,这也是exit模块的制作者知道的,他这样做的到底我们不清楚。在我们tc2.0的环境中编写的这样的程序必定会导致重复调用只有一个ret指令的函数做的将是无用功。这是表面上看到的,因为我们完全可以不要X函数和那三条call指令。这样这个X函数是干什么的其实已经研究到底了。
这只是我们看到的,但是这样做肯定有其我们不知道的更高的道理在里面。所以找到tc2.0库函数源代码一看,才知道其真正用意,以下为exit函数介绍原话,摘抄时发现源程序字迹不是太清楚:
exit 终止程序
用法 void exit(int status);
说明 exit终止调用进程。在退出以前,所有文件被关闭,缓冲输出(正等待输出)内容被写完,所有以等级的“出口函数”(有atexit记入)被调用。status被用来提供调用进程的出口状态,一般说来,值0表示正常出口,非0值表示有错误发生。
返回值 无返回值
源程序:
#include<stdlib.h>
extern int _atexitcnt; /*count of atexit function pointers*/
extern atexit_t _atexittbl[]; /*array of atexit function pointers*/
static void dummy(void) {} /*这就是那条ret指令的原型函数。还有隐藏函数怎么写大家知道了么? 这是我写的不是原文*/
void (* _exitbuf)(void ) = dummy;
void (* _exitfopen)(void ) = dummy;
void (* _exitopen)(void ) = dummy;
void exit(int c)
{
/*Execute “atexit” functions*/
while(_atexitcnt--) (*_atexittbl [_atexitcnt])();
/*flush and close files and streams*/
(*_exitbuf)();
(*_exitfopen)();
(*_exitopen)();
_exit(c);
}
这段代码不难相信很快能看明白,这个dummy函数就是X函数,dummy函数意思是假的、虚的、预留的。
意思是,一个逻辑必须有几部分构成,但是现阶段某些部分没有任何实质内容,但是这些内容又必须存在。比如城市规划,现阶段不需要建设地铁,但是20年后建地铁是必然的事,若当前不将修建地铁的空间预留出来,则20年后修建地铁会花费更多的资源。
这也是编写程序中的一种编程思想。就是预留接口,现在用不到这个接口但是要预留,为了以后不必进行大的系统修改就能用上此接口做准备。 比如exit函数必须要关闭一些资源,这些资源分为_exitbuf,_exitfopen ,_exitopen三类,关闭这三类资源则完成了exit所有应该干的事。虽然现在没有_exitbuf,_exitfopen ,_exitopen资源需要释放,但是为了以后用到,接口还是要预留的。
已经研究完毕,同学们大家明白了么?
看的过程中如果有不明白的地方自己动手实践一下理解起来会很容易得多。
发表日期:12/06/24 00:00
网友评论(0)
当前1/1页 首页 上一页下一页 尾页