2005年4月28日星期四

head.s 分析[原创]

2005-04-28 BY:LinuxRen
head.s就运行在32位保护模式下面了,这里是名副其实的内核了

GNU 的汇编直接数,比如 0x10,如果前面有$,即$0x10,则表示立即数,在16位实模式下,内存地址由段基地址左移四位加上段内偏移量组成,而在保护模式下,由段选择子 (逻辑地址的高16位 )(由段选择子选择的描述符所决定的段基地址不需要左移,直接加偏移地址就可以形成线性地址)和段内偏移量(逻辑地址的低16位)组成,这里是内存管理中 的段式管理,还有页式管理(以后再说),这里有张图片可以很好地说明段式管理:

如果仅仅是0x10,则表示内存的地址。

这张图片好像有个小问题,右上角的偏移量应该是32位的,这里很容易给人错觉好像偏移量就是逻辑地址的低16位,段选择子时逻辑地址的高16位,可以从intel的手册得到证实 其中的图5-2,呵呵,intel应该不会犯错吧

为什么既有段式内存管理,也有页式内存管理,我想大概是cpu设计的原因,如果cpu加电直接就可以进入32位,那么段式管理就应该报废了。也不知道现在的64位的cpu是怎么处理的,现在64微处理器的资料实在太少了。

head.s 第18行,movl $0x10, %eax 之后eax就相当于段选择子,与gdt表的地址进行运算,就可以定为一个全局描述符,在全局表述符中会给出对应段的段基址,再加上已知的段内 偏移量,就可以将一个逻辑地址转为一个线性地址,这里的0x10仅仅是段选择子。

然后call setup_idt, setup_idt代码在79行,就是将idt中的每一项都设置为指向同一个中断门ignore_int,即打印一段信息“Unknown interrupt”

在这里说一下idt,idt就是中断描述符表,和全剧描述符表是一个等级的,相应的中断描述符表项和全局描述符表项是对应的,中断门ignore_int是和全局描述符表项所表述的段一个等级的,只不过这里的不是一个段,而可以是一段代码,这样就应该容易理解了吧。

接 着call setup_gdt,gdt在前面已经临时设置过了,在这里要重新设置.这里将gdt还是设置成为含有256个全局描述符表项,第0个不用,第一个系统代 码段,第二个系统数据段,值分别是 0x00c09a0000000fff 0x00c09020000000fff,可以对照着张表来看看具体的意思:


这里将系统代码段和系统数据段的段长都设置成为了16M.

后面的252项都填充0,用于以后用户程序使用。

接下来就是检查数学协处理器,还有什么287/387,没看,跳过。

然后就是开启分页管理

这里的分页机制是专门为内核使用的,也就是说以后的应用程序并不适用这里的页表,而是在程序执行的时候自己自动加载自己的页表。

这里的分页比较简单,经过分页之后的物理地址是和分页前的线性地址相同的,这一点是非常重要的。因为在分页启动之前有一句话: pushl $_main
这里是将main函数的地址压栈,当分页启动之后要讲这个地址pop出来,并且跳到main去执行,系统就会跳到以前压入栈的main的地址经过分页映射后的地址去运行,如果物理地址和线性地址不一样的话,那么就会跳到错误的地方,但是这里不会。

[原创]setup.s分析

setup.s会将被bootsect.s读取到0x10000处的system模块移动到0x00000处,这样好象会把中断表给覆盖掉,文件执行到后来会加载中断描述符表,但是idt表却是这个样子的:

idt_48:
.word 0
.word 0,0

idt表的基地址居然还是0x00000,这里就搞不懂了,因为这个时候0x00000的地方已经是system模块了,所以在真正的重新设置idt表以前,如果出现异常,我不知道系统将会进行什么样的处理,这里是问题一

setup.s会读取大量的参数然后存到0x90000处,也就是说会覆盖bootsect.s,具体的参数分布书上44页有,这里有个问题是,系统会将 第一个硬盘的参数表读到0x90080处,然后再读取第二个硬盘的参数表到0x90090处,然后再去检测到底有没有第二个硬盘,如果没有再将 0x90090处的参数表清0,怪了,为什么不先检测有没有第二个硬盘,在去决定是否读取参数表到0x90090呢?非要反着来不成?

下面是我对保护模式的一点理解,不知是否正确,有错误欢迎提出:

在16位的实模式下的程序在内存中的布局和操作系统的是混在一起的,也就是说,cpu没有提供对操作系统的保护,这会出很多问题,32为保护模式下, cpu提供的很强大的功能来保护操作系统的代码不被侵犯,我们所要做的就是提供相应的数据,将相应的寄存器初始化,打开A20地址线,然后执行一个跳转指 令,cpu就会进入保护模式,对我们的程序进行保护,具体的各位可以在网上找到资料,其中一个方法就是设置gdt以及ldt来保护操作系统以及应用程序, gdt(大小限制在64k以内)也即全局描述符表里包含有全局描述符,每个全局描述符得大小为8字节,所以理论上,一个gdt一共可以有64K/8=8K 个全局描述符.其中三个是描述全局性的操作系统的代码段,数据段,其他的一个程序占用一个全局描述符.

系统怎么来确定要使用哪一个全局描述符呢?使用段选择子!这里是他的结构:

其中3-15字节是用来索引gdt来去定某一个全局描述符得,共13位,所以gdt最大长度限制在2^13=64K.但是不知道为什么在这里Linus只将gdt的长度设置为了2K,也就是为什么在
gdt_48:
.word 0x800 !这里为什么是0x800,而不是0xFFFF,难道就是因为现在的gdt是临时的,以后还有设置,所以只要够现在用就可以了??

.word 512+gdt,0x9 ! 这里由于setup.s是在0x90200处,所以要加上0x200(512)的偏移量。

然后就是为系统进入32为保护模式作准备了,这里就要先初始化gdt,由于现在还没有程序运行,所以只是用了三项(其实是两项),其中,第零项没有使用,第一项描述系统的代码段,第二项描述系统的数据段(堆栈段)。

全局表述符表:

gdt:
.word 0x0000 !第0个弃用
.word 0x0000
.word 0x0000
.word 0x0000

.word 0x07FF !第1个,第0个用了4个word,所以这里偏移量是0x08
.word 0x0000 !用来描述系统代码段
.word 0x9A00
.word 0x00C0

.word 0x07FF !第2个,偏移量是0x10,也就是16
.word 0x0000 !用来描述系统数据段
.word 0x9200
.word 0x00C0

这里是gdt的结构



这是,进入保护模式所要准备的数据已经够了,接下来就是加载gdt的基地址以及大小限制到专用寄存器gdtr,idt基地址以及大小限制到idtr,然后打开A20数据线,关于A20数据线,哈工大纯c板块上有很好的文章介绍,然后重新对8259进行编程(看见头大,没怎么看),设置状态字,然后执行
jmpi 0,8 !这里的8就是上面的段选择子的值,化为二进制为 0000,0000,0000,1000 这里的1就是第1个全局描述符,即系统代码段,就会去执行head.s

最好对这个时候内核内存布局有个印象,便于以后的理解

-------------------
数据段表述符
-------------------
代码段描述符
-------------------
-------------------0X90200
硬件参数
-------------------0X90000



head.s程序
--------------------0X00000

[原创]内核引导启动程序分析

主要参考用书:《linux内核完全分析》
由于比较简单,所以先把bootsect.s看了一下,其实整个内核引导程序有三个,分别是bootsect.s,setup.s,head.s ,前两个都不属于严格意义上的kernel,只不过是对kernel的启动做一些准备,真正的内核是从head.s启动的,现在先分析 bootsect.s:

当你按机箱上的power on的时候,cup会自动去到bios中地址0xFFFF0处开始执行bios中的代码,除了会进行一些初始化工作,这段代码最主要的是将可启动设备的第 一个扇区(bootsect.s)(引导扇区,512字节)读到内存0x07c00处。

当bootsect.s执行时,它已经被读到了0x07c00处,首先它会将自己读到0x90000处,为什么要先读到0x7c00,再读到 0x90000,是由于bios被设置成了先读到0x7c00的地方,再度到0x90000是为了防止后来读入到0x10000处的system模块覆盖 掉bootsect.s。将自己读到0x90000处后先会设置一下堆栈(0x9000:0xff00),然后将setup.s读入到紧接着 bootsect的地方(0x90200),默认的是从软盘读取的(硬盘在当时估计是奢侈品),然后就是取相应磁盘(被当作软盘,写成了hard code)的参数,然后就是利用bios的0x10中断,ah=0x03,将一段话"Loading system..."打印到屏幕上。然后检测要使用哪个根文件系统设备(软盘或者硬盘),具体的方法是判断508,509字节处的根设备号是否被置为1.
问题一.508,509是什么时候设置的?

如果有硬盘的话,就将其保存,否则读取每磁道的扇区数来判断是什么类型的软盘(1.2M还是1.4M),也保存。
问题二.这里硬盘参数ROOT_DEV=0x306,意思是第二个硬盘的第一个分区,为什么是第二个硬盘?

问题三.在这之前的代码都是软盘启动的,是不是就是说,不管你有没有硬盘,都必须从软盘启动,如果不修改bootsect的代码的话

最后执行 jmpi 0,SETUPSEG,跳转到setup.s程序的开始处执行。


2005年4月21日星期四

今天新买了个域名

LinuxRen.Org,58块钱,现在就有两个域名了,我对.net的注册商彻底失望了,打算快到期的时候转注册商,LinuxRen.Org的解析到首页上,LinuxRen.Net直接解析到论坛上面,这样也方便一些,与空间在一个地方买的,兄弟给推荐的人,哈哈,没有这个贱人我都不知道该怎么弄了,网站就打算先这样办下去,慢慢人估计会多起来的,关键要有自己的特点,由于自己水平不高,所以还是在网上招聘版主,嗯,大家一起努力~~~~~~~~~

2005年4月19日星期二

BIOS和DOS建立的中断向量表

绝对地址 16进制 10进制 有关内容
00H 0H 0 0做除数时处理器发出的中断 溢出条件是:

除法类型 结果
---------------------------------
有符号字 +127
有符号双字 +32767
无符号字 | >255
无符号双字 | >65535
---------------------------------
DOS设有INT 00H处理程序,系统将显示"Divide Overflo",结束当前程序的执行。

04H 1H 1 单步调试时处理器发出的中断
08H 2H 2 非屏蔽中断
0CH 3H 3 调试程序设置断点时处理器发出的中断
10H 4H 4 发生算术溢出时处理器发出的中断
14H 5H 5 调用BIOS的屏幕拷贝操作
18-1FH 6-7H 6-7 保留单元
20H 8H 8 每1/18.2秒定时器发出的中断
24H 9H 9 按压或释放键时产生的中断
28H 0AH 10 保留单元
2CH 0BH 11 通讯设备使用的硬件中断
30H 0CH 12 通讯设备使用的硬件中断
34H 0DH 13 交替打印时硬件产生的中断
38H 0EH 14 软驱操作结束时产生的硬件中断
3CH 0FH 15 打印机发出警告信号时产生的硬件中断
40H 10H 16 BIOS的显示I/O功能调用
44H 11H 17 BIOS设备确认调用
48H 12H 18 BIOS确认内存空间大小的功能调用
4CH 13H 19 BOIS的磁盘I/O功能调用
50H 14H 20 BIOS的RS-232串行I/O功能调用
54H 15H 21 在PC和XT机上是BIOS磁带I/O功能调用。在AT机上是AT扩充服务功能调用。
58H 16H 22 BIOS的键盘I/O功能调用
5CH 17H 23 BIOS的打印机I/O功能调用
60H 18H 24 ROM的BASIC解释和程序功能调用
64H 19H 25 BIOS的装载引导服务调用
68H 1AH 26 BIOS的日期时钟功能调用
6CH 1BH 27 Ctrl+Break处理程序功能调用。当键入Ctrl+Break键时指向可执行的程序入口初 始化BIOS使该向量指向一条TRET指令。用户可修改该向量,使它指向自己的程序。
70H 1CH 28 指向每1/18.2秒时可执行的服务程序的入口。初始化时该向量指向一条IRET指令。用户可修 改该向量,使它指向自己的Ctrl+Break 处理程序。
74H 1DH 29 指向显示控制器初始化参数。BIOS使这个向量指向ROM驻留表。
78H 1EH 30 指向软盘参数表。BIOS使这个向量指向ROM 驻留表,但是DOS把它改为指向DOS的RAM 驻留表。
7CH 1FH 31 指向一点阵表。在这个表中,BIOS可以找到字符集后128个字符的点阵
80H 20H 32 终止程序的DOS功能调用
84H 21H 33 任何种DOS功能调用
88H 22H 34 指向DOS的结束地址
8CH 23H 35 指向D
OS的Ctrl+Break处理程序
90H 24H 36 指向DOS的严重错误处理程序
94H 25H 37 DOS绝对磁盘读调用
98H 26H 38 DOS绝对磁盘写调用
9CH 27H 39 程序终止,但仍驻留内存的DOS功能调用
9DH 28H 40 DOS空闲
9EH 29H 41 支持驱动器程序输出
9FH-A1H 2AH-2CH 42-44 保留单元
A2H 2DH 45 DOS构件接口
A3H 2EH 46 COMMAND.COM退回入口
A4H 2FH 47 多路中断(空闲信号)
A5-FFH 30-3FH 48-63 为DOS保留的单元
100H 40H 64 保留单元
104H 41H 65 指向硬盘0的参数表,BOIS使这个向量指向ROM驻留的表。
108-10FH 42-43H 66-67 保留单元
110H 44H 68 PC机使用,用于指向低分辩率图形字符参数表
114H 45H 69 保留单元
118H 46H 70 指向硬盘1的参数表,BIOS使这个向量指向 ROM驻留的表。
11CH 47H 71 保留单元
120H 48H 72 PC机使用,用于把PC机的键盘代码变换为标准的键盘代码。
124H 49H 73 指向键盘增强服务变换表
128-17FH 4A-5FH 74-95 保留单元
180-19FH 60-67H 96-103 为用户程序保留的单元
1A0-1BFH 68-6FH 104-111 未使用
1C0H 70H 112 硬件中断(IRQ--interrupt request) 8--实时时钟中断
1C4H 71H 113 硬件中断9
1C8H 72H 114 硬件中断10
1CCH 73H 115 硬件中断11
1D0H 74H 116 硬件中断12
1D4H 75H 117 硬件中断13--BIOS把这个中断向量重定向为非屏蔽中断(NMI)
1D8H 76H 118 硬件中断14
1DCH 77H 119 硬件中断15
1E0-1FFH 78-7FH 120-127 未使用
200-217H 80-85H 128-133 为BASIC保留
218-3C3H 86-F0H 134-240 BASIC程序运行时提供给BASIC解释程序作用
3C4-#FFH F1-FFH 241-255 未作用

2005年4月15日星期五

有一种爱叫痛 有一种爱叫放手

有一种爱很凄迷,有一种爱只能远望,有一种爱注定成传奇.......
 有一种爱叫做痛,痛得心脏起了褶子,痛得头脑空洞无物,痛得意志麻木萧瑟......
 有一种爱叫放弃,明知道许多事情是没有答案的,却想寻找一个答案,真的好累......
 有一种爱叫忍让,忍让也是一种爱,以爱的方式善待对方的缺陷,用包容的胸怀宽恕自己的爱人,给他一个悔悟的机会留一个自省的空间于平平淡淡中演绎经典,在无声无语中融洽恩爱.这样即使是不传奇的爱情也将变得永恒,再平淡的婚姻,依然一如既往令人留连.
有一种爱,叫放手.曾经天真的以为不管时间和空间的距离有多长多远,感情一定会恒久不变,因为爱是没有理由的......
 爱不能成为牵绊,所以要选择放手,从容的让彼此走彼此的世界,凡事到极至,伤也会痛.其实爱过就会懂,彼此个性的太过坚强终究会是一起生活的阴影。
 昨日的幸福以成为一种痕迹,两人能携手走完整人生固然很好,可陪上了一段也应心存感激了.
 爱一个人不是要成为所爱人的牵绊,只要心中有爱,生活总是那么美好!
 相距是一种缘,相识,相恋更是一种缘分,缘起而聚,缘尽而散,放手才是真爱.有一种爱叫放手!
 有一种爱,叫离开,曾经以为自己的爱情能够长久,曾经以为真心的付出就能换来幸福,其实错了......
 爱情给的唯一的东西就是背叛,无情的背叛!曾经是那么相爱的两个人,转眼陌路,留下的是残缺不全的记忆和心痛.
 没有想到结局会是这样,曾经的山盟海誓,曾经的天长地久,转眼都成了飞灰。
 经常惊醒于午夜梦回的黑暗中,我的心都好痛,我思念一个人的疼痛,看着空中的星星,想着远方属于你的夜,你还好吗?一直都快乐吗?没有我在你身边是不是有另外一个人去关心你,爱你呢?
 我现在唯一的愿望只是希望再见你一面,但我又怕见你,怕见到你,我的心又会再一次的被捏碎.我只有对自己说不要再去想他,不要再想了,虽然他的影子从未离开过。
生活还是要过的,其实有种爱叫离开,再见了,我的爱人......
 有一种爱,我们不能称之为爱情,虽然有同样的心动,同样的怀想,同样乍然相见的喜悦,依依不舍的眷恋,但世间总有一种约束,让心思沉静,让感情不再漂泊,依然可以在午夜梦回事心生柔情,依然可以相信自己的完美与可爱,在这些温柔的情愫里。
  有一种爱,可以默默的爱,默默的理解,默默的在心里装满祝福,挥一挥手,让春草缠绵,落江成阵,就是有这样的感情啊!飘荡成缠绵而温暖的空气,就是在这样 无心的眷恋里,我们认识自己也认识世间,就是有这样无缘而有情的瞬间,让我们轻轻的叹息,深深的爱,虽然我们相爱,但不能称之为爱情,多想让爱发出声响 啊!可那是一种毁灭,善良绝对不允许这样,多想让爱明白啊!可怎么人心看着爱有为难,感情也不能饶恕爱的胡来。
 就这样默默的去爱,永远放在心头来爱;当风吹来的时候,就让湖水激动地涌出堤岸,一点点就已经足够了,就象喝第一口茶水,才能品出味道.只要轻盈的湖水永远不下沉,只要坚固的堤岸永远拦得住湖水,相信湖水总是会泛出激情,堤岸也总是能感到坚强。
 有一种爱,永远难以启齿,可这种爱情来得持久,来得绝美......

第一次见老头

昨晚第一次见公司的CEO,给我们这些新进来的人开了个会,美籍华人,好像生于湖南,挺牛气的,自我介绍就说SQL框架是由他带队设计开发的,厉害!当年的IBM全球副总裁,普林斯顿大学博士毕业, 是美国电脑界声望最高、职务最高的华人 。想当初公司刚刚成立的时候就是靠的他的个人魅力吸引来了一大帮的牛人,短短1年半已经很有名气了,真得不容易。然后就是一些慷慨激昂的话,归根结底就是让我们这些人工作再苦一些,再累一些...听到最后都不耐烦了,和小学时候听校长报告一样,没啥区别,一个字:累!

在网上找的一些资料:

*刘英武
1941年出生于湖南长沙
1965年获台湾大学电机学士学位。
1969年获美国普林斯顿大学电机学和计算机学博士学位。
职业:
1955年至今:威科公司(Walket Interactive Systems)董事长、总裁和首席执行官
1993——1995年:凯登斯(Cadence Design Systems)首席执行官
1989——1992年:宏基关系企业决经理与宏基北美洲和欧洲总公司董事长和首席执行官
1969——1989年:IBM公司(以下摘要)
应用软件事业部总经理
办公室系统副总裁
公司管理委员会秘书长
通讯软件部主任电脑研究部经理
兼任:加州影片委员会委员
cadence、Triden、ASE三家公司董事

空间开通了

今天网站正式开通,放了个discuz的论坛,还不错,速度比较块,不过 刚才死活访问不到,估计又是域名解析速度慢,没办法,当时贪图小便宜找了一家小的网站注册,没想到速度 chaooooooooooooooooooooooooooooooooooooooooooooooo级慢,有时候还会报说找不到域名, kao~~~~~~~~~~~~~~~

想看的一些书

没有时间看,不过还是列出来,现在最希望的就是有那么一大段时间,用来看看书。按优先级列出来,不过是并行处理:

LINUX内核完全注释》这个赵郡是在太牛了,给我们这些浮躁的小青年们树立的一个极好的榜样,想学知识,首先心态要端正,http://www.oldlinux.org/ 作者的网站,很不错,有什么问题可以亲自询问赵老师,保证教到你完全明白为止。这本书也是我毕业设计要啃的书。目前这本书正在翻译成英文的,估计今年中旬出版。看这本书最好伴着《Unix操作系统设计》或者《操作系统概念》(英文的第七版已经出来了,国内还没得卖)

深入理解Linux内核(第二版)》不多说了(因为还没看-_-#),圣经级的。翻译的还可以(据说)

Linux内核源代码情景分析 上下》又是中国人的书,并且是难得的好书!看了内存管理一章,爽!不过还不够底层,所以转而看完全分析了,呵呵

UNIX环境高级编程》如果你用过linux,并且在linux上写过C程序,这本书你就一定听说过,大牛!尤其是作者W.Richard Stevens[已故],他的每一本书我都打算好好读一下。

UNIX网络编程卷2:进程间通信(第2版)(英文影印版)》Stevens系列

UNIX 网络编程(第2版)第1卷:套接口API和X/Open 传输接口API》Stevens系列

TCP/IP详解三卷》Stevens系列

深入理解计算机系统(修订版)》 底层的一些东西,“所有想写出更快、更可靠程序的开发人员必读之书!”

Linkers & Loaders》好像没有纸版本的,我把地址贴到了LinuxRen上,看看吧,对理解程序的执行很有帮助的。

中断没有被屏蔽,J2EE类的书籍可能要"陷入"进来

2005年4月11日星期一

80386 ASM程序设计基础–80386实模式下编程III

主要介绍段描述符,段选择子
在保护模式下,段是实现虚拟地址到线性地址转换的基础。在保护方下,每个段有三个参数:段基址,段界 限,段属性。段基址规定了线性地址空间中段的开始地址,段基址长度为32位,所以任何一个段都可以从32位线性地址空间中的任何一个字节开始,这一点和实 式方式不同,实式方式下要求段的边界必须被16整除。段界限规定段的大小,段界限用20位表示,而且段界限可以是字节或4K为单位,这个称为段的粒度。当 段界限以字节为单位时,那么段的范围是1字节至1M字节;当段界限是以4K字节为单位时,那么段的范围是4K至4G。段的界限同时也是用来校验偏移地址的 合法性,比如说段A的基址为00123456H,段界限为1000H,如果段界限以字节为单位,那么段的范围是00123456H-00124456H; 如果段界限以4K字节为单位,那么段的范围是00123456H-00223456H。事实上,段的界限也可以用来校验偏移地址的合法性,上面的例子中界 限为1000H,那么偏移地址的范围就是0-1000H,如果偏移地址不在这个范围内那就会引起异常。需要说明的是,数据段有点特殊,因为数据段的偏移范 围不仅仅是由段界限来决定,还要由段的扩展方向(Extension Direction)来决定,因为要照顾到堆栈段(堆栈段是一种特殊的数据段,它是向低端地址扩展的),如果段界限为Limit,段的扩展方向为向高端地 址扩展的话,那么我们可以断定它是一普通的数据段,0-Limit是有效的偏移范围,而Limit以上属于无效的偏移范围;如果段界限为Limit,段的 扩展方向为向低端地址扩展的话,那么可以断定它是一堆栈段,此时0-Limit是无效的偏移范围,Limit以上则属于有效的偏移范围,正好和向高端地址 扩展的普通数据段相反。除了堆栈段以外,其它的段均是自然向高端扩展。
段基址,段界限及段属性这三个参数在保护模式下用描述符来描述,每个描述符的长度为8个字节,每个段都有一个对应的描述符。在保护模式下有三种描述符:存储段描述符,系统段描述符,门描述符。
A.存储段描述符:存储段是指程序直接执行的代码段和数据段,存储段描述符是用来描述存储段的,也可以说是用来描述代码和数据段的,它的长度为8个字节,该描述符结构示意图:

第7字节 第6字节 第5字节 第4字节 第3字节 第2字节 第1字节 第0字节
|--------|------------------|-----------------------------|-----------------|
|段基址的| | | |
|高8位 |Segment Attributes| 段基址的低24位 | 段界限的低16位 |
| 24~31 | 段属性,占用两 | 0~23 | 0~15 |
| | 个字节 | | |
|--------|------------------|-----------------------------|-----------------|
| |
| |
_________| |_____________________________
| 15 14 13 12 11 8 7 6 5 3 0|
|---|---|---|---|-------------|---|--- -|---|------------|
| G | D |0 |AVL|段界限的高4位| P | DPL |DT | TYPE |
|---|---|---|---|--- ---------|---|-----|---|------------|

段基址和段界限都被安排在描述符的两个域中,主要是来看段的属性:
a.G(第15位),这是段界限粒度,即是说段界限到底是以字节为单还是以4K字节为单位。G=0表示段界限是字节,G=1表示段界限为4K字节。
b.D(第14位),D是一个很特殊的位,在描述可执行段,向低扩展数据段或者由SS寄存器寻址的段。 在描述可执行段的描述符中,D位决定了指令使用的地址及操作数据默认的大小,D=1表示默认情况下使用32位地址及32位或8位操作数,这样的代码段称为 32位代码段;D=0表示默认情况下使用16位地址及16位操作数或8位操作数,这样的代码段称为16位代码段;在向低扩展的数据段中,D=1表示段的上 部界限为4G,D=0表示段的上部界限为64K;在描述由SS寄存器寻址的段中,该位决定使用隐式的堆栈访问指令使用何种堆栈指针寄存器。D=1表示使用 32位堆栈指针寄存器ESP,D=0表示使用16位堆栈指针寄存器SP,隐式的堆栈访问指令指的是那些指令中没有明显对SP或ESP进行操作的指令,比如 说PUSH,POP,PUSHA,POPA,PUSHAD,POPAD都属于隐式的堆栈访问指令。
c.0(第13位),这一位恒为0,为80386以后的处理器保留的。
d.AVL(第12位),软件可利用位,主要是为了保持和以后的处理兼容。
e.第11位到第8位是段界限的高4位。
f.P(第7位),存在位,P=1表示描述符对转换地址有效。P=0表示描述符对转换地址无效,如果使用该描述符将会引起异常。
g.DPL(Descriptor Privelege Level)描述符特权级,共2位,它规定了所述段的特权级别,用于特权检查,以决定是否能对该段进行访问。
h.DT(Descriptor Type)描述符的类型,DT=0表示存储段描述符,DT=0表示系统段描述符和门描述符。
i.TYPE,共4位,说明存储段的具体属性:
TYPE0:指示描述符是否被访问,用A标记,A=0表示描述符未被访问,A=1表示描述符已被访问。
TYPE1:根据TYPE3来确定。
TYPE2:根据TYPE3来确定。
TYPE3:指示描述符所描述的段是数据段还是代码段,用E标记。E=0表示是不可执行段,是数据段,对应的描述符也就是数据段描述符。E=1表示是可执行段,也就是代码段,对就的描述符也就是代码段描述符。
如果TYPE3=0,也就是说描述符是数据段描述符,那么TYPE1指示该数据段是否可写,用W标记。 W=0表示对应的数据段不可写,只读。W=1表示对应的数据段可写。TYPE2则指示数据段的扩展方向,用ED标记。ED=0表示向高端扩展,ED=1表 示向低端扩展。
如果TYPE3=1,也就是说描述符是代码段描述符,那么TYPE1指示该代码段是否可读,用符号R标 记。R=0表示对应的代码段不可读,只能执行,R=1表示对应的代码可读可执行。TYPE2则指示所描述的代码段是否是一致代码段,用C表示。C=0表示 代码段不是一致代码段,C=1表示是一致代码段。
TYPE3-TYPE0这四位可以列成一个表:
___________________________________________________________________________________
|0000 |只读 |
|_____|____________________________________________________________________________|
|0001 |只读,已访问 |
|_____|____________________________________________________________________________|
|0010 |可读,可写 |
|_____|____________________________________________________________________________|
|0011 |读写,已访问 |
|_____|____________________________________________________________________________|
|0100 |只读,向低扩展 |
|_____|____________________________________________________________________________|
|0101 |只读,向低扩展 |
|_____|____________________________________________________________________________|
|0110 |读/写,向低扩展 |
|_____|____________________________________________________________________________|
|0111 |读/写,向低扩展,已访问 |
|_____|____________________________________________________________________________|
|1000 |只执行 |
|_____|____________________________________________________________________________|
|1001 |只执行,已访问 |
|_____|____________________________________________________________________________|
|1010 |可执行,可读 |
|_____|____________________
________________________________________________________|
|1011 |可执行,可读,已访问 |
|_____|____________________________________________________________________________|
|1100 |只执行,一致代码段 |
|_____|____________________________________________________________________________|
|1101 |只执行,一致代码段,已访问 |
|_____|____________________________________________________________________________|
|1110 |可执行,可读,一致代码段 |
|_____|____________________________________________________________________________|
|1111 |可执行,可读,一致代码段,已访问 |
|_____|____________________________________________________________________________|
存储段描述符的结构可以这样定义:
DESCRIPTOR STRUCT
Segment_LimitL16 DW 0;段界限的低16位
Segment_BaseL16 DW 0;段基址的低16位
Segment_BaseM8 DB 0;段基址的中间8位
Segment_BaseH8 DB 0;段基址的高8位
Segment_Attributes DW 0;段属性
DESCRIPTOR ENDS
一个任务有多个段,每个段都有一个描述符。因此在80386下,为了方便管理这些段描述符,将描述符组 成一个线性表,称之为描述符表。在80386下有三种描述符表:GDT(Global Descriptor Table),LDT(Local Descriptor Table),IDT(Interrupt Descriptor Table)。在整个系统中全局描述符表GDT和中断描述符表只有一张,局部描述符表可以由若干张。每个描述符表都形成一个特殊的16位数据段,这样的特 殊数据段最多可以有8192个描述符,具体使用哪一个段描述符,由段的选择子来确定。每个任务都有自已的局部描述符表LDT,它包含自已的代码段,数据 段,堆栈段,也包含该任务使用的一些门描述符。随着任务的切换,LDT也跟着切换。GDT包含每一个任务都可能或可以访问的段的描述符,通常包含描述操作 系统所用的代码段,数据段以及堆栈段的描述符,也包含描述任务LDT的描述符。在任务切换时,并不切换GDT。一个任务的整个虚拟地址空间可以分为相等的 两半,一半空间的描述符在全局描述符表GDT中,一半空的描述符在局部描述符表LDT中。由于全局描述符表和局部描述符表都可以包含最多为8192个描述 符,而每个描述符所描述的段的最大长度为4G,因此最大的虚拟地址空间为:8192*4G*2=64TB。
段选择子用来确定使用描述符表中的哪一个描述符。实式模式下逻辑地址由段地址*16再加上段内偏移地 址;保护模式下虚拟地址空间由段选择子和段内偏移来确定,和实式模式比较,段选择子代替了段值,实际上通过段选择子就可以确定了段基址。选择子的高13位 是描述符表中的索引号,用来确定描述符,因为是13位,所以说最多可以有2的13次方8192个描述符,索引号:0-8191。标记TI指示是从全局描述 符中读取描述符还是从局部描述符表中读取描述符。TI=0指示是从全局描述符表中读取描述符,TI=1指示从局部描述符表读取描述符。RPL表示请求特权 级,用于特权检查。假设段选择子为88H,则表示请求的特权级别是0,从全局描述表中读取描述表,描述符的索引号为11H。有一个特殊的选择子称为空选择 子,它的Index=0(即高13位为0),TI=0,RPL则可以为任意值。当用空选择子对存储器进行访问,会出现异常。空选择子对应于全局描述表中的 第0个描述符,因此全局描述符表中的第0个描述符总是不会被访问。如果TI=1,那么就不是空选择子,它指定的是当前局部描述符表中的第0个描述符。为了 更快地从段选择子中获得段的基本信息(段基址,段界限,段属性),从80386开始为每个段寄存器在硬件上配备了段描述符高速缓冲存储器,对我们写程序的 人来讲,它是不可编程的。有了这种高速缓冲寄存器后,每当将选择子装入段寄存器后,处理器将自动装入描述符表中相应的描述符,并将描述表的信息装入到高速 缓冲寄存器,这样可以加快访问速度,以下是段选择子的结构示意图:

15________________________________________________________________3__2__1_____0
| |TI | RPL |
|________________________________________________________________|___|________|

80386 ASM程序设计基础–80386实模式下编程II

虽然80386处理器要较以前的处理器的功能大大增强,但这些功能只能在保护模式下才能全部得到发挥。在实模式下最大寻址空间只有1M,但在保护模式最大 寻址空间可达4G,可以访问到所有的物理内存。同时由于引入虚拟内存的概念,在程序设计中可使用的地址空间为64TB。80386处理器采用了可扩充的分 段管理和可选的分页管理机制,这两个存储管理机制由MMU(Memory Management Unit)部件来实现。因此,如果在80386下进行实模式编程,这时的80386处理器相当于一功能更强大,运行速度更快的8086处理器。80386 提供对虚拟存储器的支持,虚拟存储器的理论基础就是:速度非常快的内存储器和海量的外存储器,所以它是一种软硬件结合的技术,它能够提供比物理内存大得多 的存储空间。
80386下的段具有三个属性:段基址,段界限,段属性,通常描述段的称作段描述符(Segment Descriptor),而描述符通常放在一个线性表中,这种线性表又分为:GDT(Global Descriptor Table),LDT(Local Descriptor Table),IDT(Interrupt Descriptor Table),通常用一个叫做选择子的东西去确定使用上述三个线性表中哪一个描述符。程序中使用的地址空间就是虚拟地址空间,上面已经说过80386下虚 拟地址空间可达到64TB(后面将解释为什么可以达到64TB),虚拟地址空间由一个选择子和段内偏移组成,这是因为通过段的选择子我们可以得到该段的描 述符,而在描述符中又说明了段的基址,段的界限及段的属性,再加上段的偏移就可以得到虚拟地址空间。不过请注意,这里并没有将段基址乘以16再加上偏移地 址,这是保护模式与实式模式的区别之一。很明显,任何数据都必须装入到物理内存才能够被存储器处理,所以二维的虚拟地址空间必须转换成一维的物理地址。同 时,由于每个任务都有自已的虚拟地址空间,为了防止多个并行任务将虚拟地址空间映射同一物理地址空间采用线性地址空间隔离虚拟地址和物理地址,线性地址空 间由一维的线性地址构成,线性地址空间与物理地址空间对等,线性地址为32位,可寻址空间为4GB(物理地址空间最大也可以达到4GB,址址为32位,所 以说线性地址空间与物理地址空间对等)。下面是80386虚拟地址空间与物理址空间的转换示意图:

|----------| |------------| |--------| |------------------| |--------|
| 虚拟地址 |------>|分段管理部件|------>|线性地址|---|--->|可选的分页管理部件|---|-->|物理地址|
|----|-----| |------------| |--------| | |------------------| | |--------|
|------|-------| | |
| | |---------------------------|
|----------| |---------|
| 选择子 | | 段内偏移|
|----------| |---------|

地址映射过程中,通过分段管理部件将虚拟地址空间转换成线性地址,这一步是必然存在的。如果在程序中启 用了分页管理机制,那么线性地址还要经过分页管理部件的处理才得到最后的物理地址。如果没有采用分页管理机制,那么得到的线性地址就是物理地址。分页管理 部件的主要的工作机制在于将线性地址和物理地址划分成大小相同的块,通过在建立两者之间的页表来建立对应关系。分段管理机制使用大小可变的存储块,使用分 段管理机制适合处理复杂系统的逻辑分段。分页管理机制使用固定大小的块,所以它适合管理物理存储器,分页管理机制能够更有效地使用虚拟地址空间。
80386支持多任务,因此对各个任务进行保护是非常必要的,对任务的保护可分为:同一任务内的保护,不同任务之间的保护。
a.同一任务内的保护,在同一任务内定义有四种特权级别(Previlege Level),将这些特权级别分配给段中的代码和数据,把最高的特权级别分配给最重要的数据和最可信任的代码,将较低级别的特权分给一般的代码和不重要的 数据。特权级别用0~3来表示,用数字0表示最高特权级别,用数字3表示最低特权级别,在比较特权级别时不使用大于或小于,而是使用外层或里层来比较,很 明显特权级别为0表示最里层,特别级别为3表示最外层。任何一个存储段(程序直接进行访问的代码段和数据段)都有一个特权级别,在一个程序试图访问这个存 储时,就会进行特权级别的比较,如果小于或等于(如果等于表明同级,小于则表明是内层)处该存储段的特权级别就可以对该存储段进行访问。任务在特定时刻下 的特权级别称为CPL(Current Previlege Level),看一简单的结构示意图:

|---------|-------|
| CodeA | DataA | 特权级别为0
|---------|-------|
|---------|-------|
| CodeB | DataB | 特权级别为1
|---------|-------|
|---------|-------|
| CodeC | DataC | 特权级别为2
|---------|-------|
|---------|-------|
| CodeD | DataD | 特权级别为3
|---------|-------|

CodeA可以访问DataA,CodeB,DataB,CodeC,DataC,CodeD,DataD
CodeB可以访问Datab,CodeC,DataC,CodeD,DataD,但不可以访问CodeA,DataA
CodeC可以访问DataC,CodeD,DataD,但不可以访问CodeA,DataA,CodeB,DataB
CodeD处在最外层,只能访问同级的DataD,不可以访问CodeA,DataA,CodeB,DataB,CodeC,DataC
通常应用程序放在最外层,但由于每个应用程序的虚拟地址空间不同,因此它们被隔离保护。这种特权级别的 典型用法就是:将操作系统的核心放在0层,操作系统的其余部分放在1级,2级留给中间软件使用,3级放应用程序,这样的安排的好处在于:操作系统的核心因 为放在0层,因此它可以访问任务中所有的存储段,而1级的部分操作系统可以访问除0级以外的所有存储段,应用程序只能访问自身的存储段。
b.不同任务间的保护,通过把每个任务放在不同的虚拟地址空间来实现隔离保护,虚拟地址到物理地址之间 的映射由每个任务中的映射函数来决定,随着任务切换,映射函数也跟着切换,这样可以保证任务A映射到物理内存中的区域与任务B映射到内存中的区域是不同 的,尽管有可能它们的虚拟地址空间相同,但它们最终在物理内存中的位置是不同的,从而起到了保护作用

80386 ASM程序设计基础–80386实模式下编程

80386实模式下编程
80386在实模式下是一个更快的8086,它不但可以进行32位操作,而且还可以进32位寻址,并且 还可以使用80386的扩展指令。不过,由于是在实模下,寻址的最大空间为1M。在一个段内,段的最大长度不超过64K,否则就会发生异常。
在8086下定义一个段的完整格式是:
段名 [定位类型] [组合类型] [‘类别’]
80386下定义一个段的完整格式是:
段名 [定位类型] [组合类型] [‘类别’] [属性类型]
说明:属性类型有两种:USE32和USE16,USE32表示32位段,USE16表示16位段。如 果你在程序中用到伪指令.386,那么默认的属性类型就是USE32(32位段),如果没有用伪指令指定CPU的类型,那么默认的属性类型就是 USE16,在实方式下只能使用16位段,即用USE16。
eg:
CSEG PARA PUBLIC USE32;定义一个32位的段
AA DW ?

BB DD ?
CC DB ?
DD DW ?
EE DW 0,0,0.....
CSEG ENDS
由于在80386中用到了66H操作前缀和67H地址前缀,因此尽管在实式模式下,只要设定的CPU类 型是80386,仍然可以进行32位操作,可以进行32位寻址,66H,67H这两个前缀无需程序员在程序中书写,汇编程序会自动加上的。只要在程序中对 32位操作数进行访问,或进行32位寻址,那么就会加上操作数前缀66H和地址前缀67H。相反,如果在32位段中对16位或8位的访问,汇编程序中也会 加上这两个前缀。
下面将给出一个例子程序,演示一下在80386的实模式下编程的方法与技巧(这是从网上down的一个程序,不是我写的,但我会作详细的解剖,并与8086下的程序设计作出比较):
用十进制,十六进制,二进制三种形式显示双字存储单元F000:1234中的内容
|------------------MAIN PROC------------|
| .386 |
| code segment para public 'code' use16 |
| assume cs:code |
| begin: |
| mov ax,0f000h |
| mov fs,ax |
| mov eax,fs:[1234H] |
| call todec |
| call newline |
| call tohex |
| mov al,'H' |
| call echo |
| call newline |
| call tobin |
| mov al,'B' |
| call echo |
| call newline |
| mov ah,4ch |
| int 21h |
|---------------------------------------|
;sub-function todec
todec proc near
pushad
mov ebx,10
xor cx,cx
dec1:
xor edx,edx
div ebx
push dx
inc cx
or eax,eax
jnz dec1
dec2:
pop ax
call toasc
call echo
loop dec2
popad
ret
todec endp

;sub-function tobin
tobin proc near
push eax
push ecx
push edx
bsr edx,eax
jnz bin1
xor dx,dx
bin1:
mov cl,31
sub cl,dl
shl eax,cl
mov cx,dx
inc cx
mov edx,eax
bin2:
rol edx,1
mov al,'0'
adc al,0
call echo
loop bin2
pop edx
pop ecx
pop eax
ret
tobin endp

;sub-function tohex
tohex proc near
countb=8
enter countb,0
movzx ebp,bp
mov ecx,countb
mov edx,eax
hex1:
mov al,dl
and al,0fh
mov [ebp-countb+ecx-1],al
ror edx,4
loop hex1
mov cx,countb
xor ebx,ebx
hex2:
cmp byte ptr [ebp-countb+ebx],0
jnz hex3
inc ebx
loop hex2
dec ebx
mov cx,1
hex3:
mov al,[ebp-countb+ebx]
inc ebx
call toasc
call echo
loop hex3
leave
ret
tohex endp

;sub-function toasc
toasc proc near
and al,0fh
cmp al,'0'
cmp al,'9'
seta dl
movzx dx,dl
imul dx,7
add al,dl
toasc1:ret
toasc endp

;sub-function newline
newline proc near
push dx
push ax
mov dl,0dh
mov ah,2
int 21
mov dl,0ah
int 21
pop ax
pop dx
ret
newline endp

echo proc near
push ax
push dx
mov dl,al
mov ah,2
int 21h
pop dx
pop ax
echo endp
剖析:
先来看主程序框架,下面就是MAIN PROC:
|------------------MAIN PROC-------------------------------|
|.386;定义处理器的类型为386表示可以使用所有80386指令 |
| code segment para public 'code' use16 |
| assume cs:code |
| begin: |
| mov ax,0f000h |
| mov fs,ax;将f000h装入段寄存器fs |
| mov eax,fs:[1234H];将1234H内存单元中的双字送给寄存器EAX|
| call todec;调用子过程todec |
| call newline;调用子过程newline进行回车换行 |
| mov eax,fs:[1234h]; |
| call tohex;调用子过程tohex |
| mov al,'H' |
| call echo;显示字符H |
| call newline; |
| mov eax,fs:[1234H] |
| call tobin;调用子过程tobin |
| mov al,'B' |
| call echo


| call newline |
| mov ah,4ch |
| int 21h |
|----------------------------------------------------------|
主程序中的内容一目了然,很简单。和8086下唯一不同的是就是要用伪指令定义CPU的类型,并且段寄存器的定义多了一个属性类型USE16,再就是32位操作,使用80386的指令,其它的和8086下没有什么区别。
重点是要分析几个过程,从网上down下来时,过程newline和toasc没有实现代码,因为这很 简单,所以上述toasc,newline,echo的过程体是由我写进去的,这两个过程体代码不多而且非常简单,就不作介绍了。重点介绍todec, tobin,tohex。
a.子过程todec,这个子过程的主要功能是将f000:1234双字单元的内容用十进制显示,下面就来看每一行代码:
|-----------------------------------------------------------|
|todec proc near |
| pushad |
| mov ebx,10 |
| xor cx,cx |
| dec1: |
| xor edx,edx |
| div ebx |
| push dx |
| inc cx |
| or eax,eax |
| jnz dec1 |
| dec2: |
| pop ax |
| call toasc |
| call echo |
| loop dec2 |
| popad |
| ret |
|todec endp |
|-----------------------------------------------------------|
分析:将一个数用十进制数来表示,要它对它进行除以10的运算,得到商和余数。再将商除以10,如此循 环直到商为0为止,在这个过程中得到的一系列的模(余数)就是十进制数系列。在主程序中,已经将f000:1234双字单元的内容放到EAX寄存器中,由 于后来要用十六进制数,二进制数显示,所以EAX寄存器的内容不允许改变,因此在子过程的一开始,要将EAX的内容先入栈,所以子过程的一开始就用 PUSHAD将8个32位通用寄存器的内容全部入栈。在标号dec1不断地进行除以10运算,将所得到的余数全部入栈,同时用cx进行计数。在标号 dec2中,逐个弹出在标号dec1中得到的余数,然后分别将它们显示出来,这样就可以将该存储单元中的内容用十进数表示,下面解释每一条指令的功能:
a1.pushad;将8个32位通用寄存器全部入栈
a2.xor cx,cx;cx清0
a3.mov ebx,10;10=>ebx<
br /> a4.xor edx,edx;edx清0
a5.div ebx;edx存放高32位,不过是0,EAX中存放低32位,即ffff:[1234]双字的内容;除法得到的商放在EAX,余数放在EDX
a6.push dx;将edx的低16位dx入栈
a7.inc cx;cx+1=>cx
a8.or eax,eax;对eax进行或操作,主要是用来判断eax是否为0,即判断商是否为0,从而判断是否应该结束标号为dec1的循环。
a9.jnz dec1
a10.pop ax;将放在堆栈中的余数逐个弹出到ax中
a11.call toasc;显示ax的内容
a12.call echo
a13.loop dec2;将所有的余数显示完毕
a14.popad;8个32位通用寄存器全部出栈
a15.ret

b.子过程tohex
PUSH BP
SP=>BP
SP<=SP-CNT1
|------------------------------------------------------------|
|tohex proc near |
| countb=8 |
| enter countb,0 |
| movzx ebp,bp |
| mov ecx,countb |
| mov edx,eax |
|hex1: |
| mov al,dl |
| and al,0fh |
| mov [ebp-countb+ecx-1],al |
| ror edx,4 |
| loop hex1 |
| mov cx,countb |
| xor ebx,ebx |
|hex2: |
| cmp byte ptr [ebp-countb+ebx],0 |
| jnz hex3 |
| inc ebx |
| loop hex2 |
| dec ebx |
| mov cx,1 |
|hex3: |
| mov al,[ebp-countb+ebx] |
| inc ebx |
| call toasc |
| call echo |
| loop hex3 |
| leave |
| ret |
|tohex endp |
|------------------------------------------------------------|
分析:该子过程的功能是将f000:1234双字单元的内容以16进制数显示出来,首先来考虑一下将一 个数以16进制数表示出来的算法,事实上在汇编语言中操作数一直都是以十六进制表示的。因此,在这个子过程中不可以像上一个子过程一样,通过不断的除法取 模得到结果。事实上,我们只需要将32位操作,以每半个字节(四位)的内容显示出来就可以了,有了这一编程思想,就很容易看懂上面的子过程。当然你们会 问,为什么要每次只显示半个字节而不显示一个字节呢?呵呵,十六进制的十六个数是从0000-1111,不就是半个字节了。所以要循环8次才可以显示出 32位的EAX,所以这里用ror指令来不断循环移位,每次右移4位放到dl的低4位中。这8个半字节分别放在[ebp-1]至[ebp-8]的存储单元 中。不过,存储的顺序是由低位到高位,如果就这样显示结果肯定显示反了。标号hex2,hex3的主要功能是用来判断f000:1234双字单元的内容是 否为0,如果为0,只需要将最后结果显示一个0即可,否则就显示出8位内容。下面是每条指令的功能:
b1.countb=8;伪指令定义一局部变量countb,其值为8
b2.enter countb,0;建立堆栈框架指令
b3.movzx ebp,bp;对bp进行零扩展
b4.mov ecx,countb;8=>ecx
b5.mov edx,eax;将eax=>edx
b6.mov al,dl
b7.and al,0fh;取低4位
b8.mov [ebp-countb+ecx-1],al;将8个半字节的内容逐一送到[ebp-1]至[ebp-8]的内存单元中
b9.ror edx,4;对edx进行循环右移,每次移动4位
b10.loop hex1
b11.mov cx,countb
b12.xor ebx,ebx;ebx清0
b13.cmp byte ptr [ebp-countb+ebx],0;下面的语句主要用来判断源操作数f000:1234的内容是否为0,如果是0,就在屏幕上只显示一个0
b14.jnz hex3
b15.inc ebx
b16.loop hex2
b17.dec ebx
b18.mov cx,1
b19.mov al,[ebp-countb+ebx];逐一显示[ebp-8]到[ebp-1]的内容。
b20.inc ebx
b21.call toasc
b22.call echo
b23.loop hex3
b24.leave;释放堆栈框架
b25.ret

c.子过程tobin
|---------------------------------------|
|tobin proc near |
| push eax |
| push ecx |
| push edx |
| bsr edx,eax |
| jnz bin1 |
| xor dx,dx |
|bin1: |
| mov cl,31 |
| sub cl,dl |
| shl eax,cl |

| mov cx,dx |
| inc cx
| mov edx,eax |
|bin2:

| rol edx,1
| mov al,'0'
| adc al,0
| call echo
| loop bin2
| pop edx |
| pop ecx
| pop eax
| ret
|tobin endp
|---------------------------------------|
分析:将一个数用二进制数显示出来,只需要用ROL指令就可以了。这里作者写的程序就是这个思路,在标 号bin1中主要判断f000:1234单元的内容是否为0,如果为0,那么只需要在屏幕上显示一个0就可以了。否则的话,就用ROL指令对源操作数移位 32位,从最高位31位到最低位逐一显示出来,程序设计思路很简单,没有什么复杂的算法,下面看每一条指令的含义:
c1.push eax;eax入栈
c2.push ecx;ecx入栈
c3.push edx;edx入栈
c4.bsr edx,eax;对eax进行扫描,并把第一个为1的位号送给edx
c5.jnz bin1;如果eax不为0,就跳到c7去执行
c6.xor dx,dx;如果eax为0,就将dx清0
c7.mov cl,31;从c7到c12主要用来设置计数器cx,如果eax=0,那么就设置cx=1,如果eax不等于0,那么就设置ecx=32
c8.sub cl,dl
c9.shl eax,cl
c10.mov cx,dx
c11.inc cx
c12.mov edx,eax
c13.rol edx,1;从c13到c15主要用来显示二进制数据,顺序是从最高位31位到最低位0位
c14.mov al,'0'
c15.adc al,0
c16.call echo
c17.loop bin2
c18.pop edx;edx出栈
c19.pop ecx;ecx出栈
c20.pop eax;eax出栈
c21.ret
在后续的篇幅里将主要介绍保护式下的段页管理机制及及如何在保护模下编程。

80386 ASM程序设计基础–高级语言支持,条件字节设置指令

高级语言支持,条件字节设置指令
AA.高级语言支持指令,开始于80186,主要是用来简化高级语言的某些特征,总共有3条指令:ENTER,LEAVE,BOUND
a.ENTER,LEAVE,建立与释放堆栈框架命令。在C语言中,栈不仅用来向函数传递入口参数,而 且在函数内部的局部变量也存放在栈中。为了准确地存取这些这些局变量和准确地获得入口参数,就需要建立堆栈框架,先看一个小程序:
//C Programming-Language
int sum(int x,int y)
{
int sum;
sum=x+y;
return sum;
}
//The corresponding ASM codes lists below
_sum proc near;注意C语言中函数参数的入栈方式是从右向左,即先是参数y入栈,再是x入栈,再是函数的返回地址入栈
push bp
mov bp,sp;建立堆栈框架
sub sp,2

mov ax,word ptr [bp+4];取参数x
add ax,word ptr [bp+6];加参数y
mov word ptr [bp-2],ax
mov ax,word ptr [bp-2]
mov sp,bp;释放栈框架
pop bp
ret
_sum endp
此时栈顶的示意图是:
|----------------------|
| BP |<====SP
|----------------------|
| 函数返回地址 |<====BP+2
|----------------------|
| 参数x |<====BP+4
|----------------------|
| 参数y |<====BP+6
|----------------------|
| ...... |<====BP+8
|----------------------|
| ........ |<====BP+n,n是一能被2整除的数
|----------------------|
如果用建立和释放堆栈框架指令,那么对应的汇编程序应该是:
_sum proc near
enter 2,0;建立栈框架
mov ax,word ptr [bp+4];取参数x
add ax,word ptr [bp+6];加参数y
mov word ptr [bp-2],ax
mov ax,word ptr [bp-2]
leave;释放栈框架
ret
_sum endp
b.建立栈框架指令ENTER,格式如下:ENTER CNT1,CNT2。其中CNT1表示框架的大小,即子程序中需要放在栈中局部变量的字节数;CNT2是立即数,表示子程序嵌套级别,即从调用框架复制到当前框架的指针数。在立即数CNT2为0时,ENTER指令的实过程是:
PUSH BP
SP=>BP
SP<=SP-CNT1
c.释放栈框架指令LEAVE,其具体实现过程:
8086:
BP=>SP
POP BP
80386:
EBP=>ESP
POP EBP
d.ENTER和LEAVE指令均不影响标志寄存器中的各标志位,同时LEAVE指令只负责释放栈框架,并不负责函数返回。因此,要在LEAVE指令后安排一条返回指令。

BB.条件字节设置指令
这是80386新增的一组指令集,将会在后面全部列表出来。条件字节设置指令的格式:
SETxx OPRD
xx是助记符的一部分,OPRD只能是8位的寄存器或存储单元。
eg:
SETO AL;表示当溢出标志位为1时,即OF=1,将AL置1,否则AL清0
SETNC CH;表示当CF=0时,将CH置1,否则将CH清0
SETNA BYTE PTR [100];表示当AF=0,将DS:[100]这一个字置1,否则将它清0
a.SETZ OPRD;等于0时(ZF=1),置OPRD为1,否则清0
b.SETE OPRD;同a
c.SETNZ OPRD;不等于0时(ZF=0),置OPRD为1,否则清0
d.SETNE OPRD;同c
e.SETS OPRD;为负数时(SF=1)置OPRD为1,否则清0
f.SETNS OPRD;同e正好相反(SF=0)
g.SETO OPRD;OF=1,置OPRD为1,否则清0
h.SETNO OPRD;同g正好相反
i.SETP OPRD;偶(PF=1)置1
j.SETPE OPRD;同i
k.SETNP OPRD;奇(PF=0)置1
l.SETPO OPRD;同k
m.SETB OPRD;低于置OPRD为1,否则清0,这是针对无符号数的
n.SETNAE OPRD;不高于即低于或等于时置OPRD为1,否则清0,这是针对无符号数的
o.SETC OPRD;CF=1,置OPRD为1,否则清0
p.SETNB OPRD;高于或等于时,置OPRD为1,否则清0,这是针对无符号数的
q.SETAE OPRD;高于时置OPRD为1,否则清0,这是针对无符号数的
r.SETNC OPRD;CF=0时,置OPRD为1,否则清0,这是针对无符号数的
s.SETBE OPRD;低于或等于时,置OPRD为1,否则清0,这是针对无符号数的,CF|ZF=1
t.SETNA OPRD;同s,这是针对无符号数的,CF|ZF=1
u.SETNBE OPRD;高于时置OPRD为1,否则清0,这是针对无符号数的,CF OR ZF=0
v.SETA OPRD;同u,这是针对无符号数的,CF OR ZF=0
w.SETL OPRD;小于时,置OPRD为1,否则清0,这是针对有符号数的
x.SETNGE OPRD;同w,这是针对有符号数的
y.SETNL OPRD;大于或等于时,置OPR为1,否则清0,这是针对有符号数的
z.SETGE OPRD;同y,这是针对有符号数的
a1.SETLE OPRD;小于或等于时,置OPRD为1,否则清0,这是针对有符号数的
a2.SETNG OPRD;同a1,这是针对有符号数的
a3.SETNLE;大于时,置OPRD为1,否则清0,这是针对有符号数的
a4.SETG;同a3,这是针对有符号数的

80386 ASM程序设计基础–控制转移指令,串操作指令

控制转移指令,串操作指令
80386控制转移指令包括:转移指令,循环指令,过程调用和返回指令。
A.转移指令包括无条件转移指令JMP和条件转移指令,无条件转移指令分为段内直接转移,段内间接转 移,段间直接转移,段间间接转移。由于80386有保护模式和实模式,在实模式下,段内转移的范围在-128~127,段间转移最大范围为64K。在保护 模式需要用48位指针,即CS:EIP(16位+32位)。条件转移指令有很多包括JCXZ,JECXZ,JBE,JAE,JA,JB等,其用法和 8086相似。

B.循环指令LOOP,LOOPZ,LOO0PE,LOOPNZ,LOOPNE,TASM支持助记符 LOOP,LOOPWE,LOOPWZ,LOOPWNZ,LOOPWNE,LOOPD,LOOPWD,LOOPDE,LOOPDNE,LOOPDNZ。以 CX作为计数器时,就可用LOOP,LOOPWE,LOOPWZ,LOOPWNZ,LOOPWNE;在以ECX作为计数器时,以LOOPD, LOOPDE,LOOPDZ,LOOPDNZ,LOOPDNE,下面的一段例子可以说明问题:
ABC PROC
MOV CX,100H
AA:
;ADD YOUR CODES HERE
LOOP AA
ABC END

C.过程调用和返回调用CALL,RET
这两个指令与8086的用法相同,但由于80386下有实模式和保护模式下。在实模式下,无论是段内调 用还是段间调用均采用32位指针,即CS:IP,它们的用法与8086下相同。在保护模式下,段间调用和段内调用均用48位指针,即ECS:IP。RET 用于返回,具体实现过程会比较复杂,在介绍完80386的地址的管理机制后会作介绍,先介绍一下以下CALL指令在8086中的用法:
a.段内直接转移,具体格式:CALL 过程名。此时CS不入栈,IP的内栈入栈,入栈后再将加上目的地址与CALL指令的下一条指令的偏移地址之差值就可以转移到目的地址,详细过程:
SP-2=>SP;将堆栈指针SP减2
(SP)<=IP;将IP进栈
IP+偏移地址之差;转到目的地址
b.段内间接转移,具体格式:CALL OPRD,那么在这里OPRD可以寄存器或内存单元,它的具体实现过程:
SP-2=>SP;将堆栈指针SP减2
(SP)<=IP;将IP进栈
IP<=(OPRD);转到目的地址
同a一样,CS不入栈
c.段间直接转移,具体格式:CALL 过程名 [FAR],此时CS,IP均要入栈,详细的实现过程:
SP-2=>SP;将堆栈指针减2
(SP)<=CS;将CS入栈
SP-2=>SP;将堆栈指针再减2
(SP)<=IP;将IP入栈
;装入新的CS,IP
IP<=过程入口的偏移地址
CS<=过程入口的段地址
d.段间间接转移,具体格式:CALL OPRD [FAR],此时CS,IP均要入栈,OPRD是32位,你知道在8086中没有32位寄存器。因此,这里的OPRD一定是存储单元,高16位是CS的值,低16位是IP值,详细的实现过程:
SP-2=>SP;将堆栈指针减2
(SP)<=CS;将CS入栈
SP-2=>SP;将堆栈指针再减2
(SP)<=IP;将IP入栈
;装入新的CS,IP
IP<=(OPRD+2,OPRD+3)
CS<=(OPRD,OPRD1)
e.段内返回
格式:RET。实际上它的实现过程:
(SP)=>IP;从当前栈顶弹出一个字,将它送给IP指令计数器
SP+2=>SP;SP
f.段间返回
格式:RET,实际上它的实现过程:
(SP)=>IP;IP出栈
SP+2=>SP;
(SP)=>CS;CS出栈
SP+2=>SP;

D.中断返回指令IRET
功能和用法与8086相同,这里顺便介绍一下8086的中断返回指令
IRET,具体的实现过程:
IP<=(SP);IP出栈
SP+2=>SP;
CS<=(SP);CS出栈
SP+2=>SP;
FLAGS<=(SP);标志寄存器出栈
SP+2=>SP;

E.串操作指令
80386在串操作指令方面增加了双字操作,在8086五条指令的基础上增加了INS,OUTS。
a.LOADSD,和8086的用法和功能相同,不过是对32位操作数操作。
b.STOSD,和8086的用法和功能相同,不过是对32位操作数操作。
c.CMPSD,和8086的用法和功能相同,不过是对32位操作数操作。
d.SCANSD,和8086的用法和功能相同,不过是对32位操作数操作。
e.MOVSD,和8086的用法和功能相同,不过是对32位操作数操作。
f.重复前缀REP,和8086的功能与用法相同,仍以CX为计数器,看下面的一小程序:
ROR ECX,2
REP MOVSD;以CX为计数器,每次传送双字
ROL ECX,1
REP MOVSW;以CX为计数器,每次传送一字
ROL ECX,1
REP MOVSB;以CX为计数器,每个传送一个字节
g.INSB,INSW,INSD,OUTSB,OUTSW,OUTSD
g1.INSB,串输入指令,以字节单位,该指令的功能是从DX指定的端口读入一个字节到ES:DI指定的内存单元中。
g2.INSW,串输入指令,以字单位,该指令的功能是从DX指定的端口读入一个字节到ES:DI指定的内存单元中。
g3.INSD,串输入指令,以双字单位,该指令的功能是从DX指定的端口读入一个字节到ES:DI指定的内存单元中。
g4.OUTSB, 串输出指令,以字节为单位,将DS:SI内存单元的内容送往DX指定的端口。
g5.OUTSW, 串输出指令,以字为单位,将DS:SI内存单元的内容送往DX指定的端口。
g6.OUTSD, 串输出指令,以双字为单位,将DS:SI内存单元的内容送往DX指定的端口。
g7.串输入和串输出指令不影响标志寄存器中的各标志位,串操作指令可以与REP一起使用

80386 ASM程序设计基础–算术运算指令,逻辑运算指令,移位指令

算术运算指令,逻辑运算指令,移位指令
AA.算术运算指令
A.加减法运算ADD,ADC,INC,SUB,SBB,DEC,CMP,NEG
a.ADD,和8086功能,用法相同,不过支持32位操作,下面的语句都是合法的。
ADD ESI,EDI
ADD EAX,DWORD PTR [1000H]
b.ADC,带进位的加法指令,即OPRDS+OPRDD+CF,其中OPRDS代表源操作数,OPRDD代表目的操作,CF代表进位标志位,功能和用法与8086相同,支持32位操作。
c.SUB,和8086相同,支持32位操作。
d.SBB,带进位的减法指令,即OPRDD-OPRDS-CF,其中OPRDS代表源操作数,OPRDD代表目的操作数,CF代表进位标志位,功能和用法与8086相同,支持32位操作。
e.DEC,减1操作,功能和用法与8086相同,支持32位操作。
f.CMP,比较操作,功能和用法与8086相同,支持32位操作。
g.NEG,求补操作,功能和用法与8086相同,支持32位操作。
h.INC 加1操作,功能和用法与8086相同,支持32位操作。

B.乘除法指令MUL,DIV,IMUL,IDIV
a.MUL,无符号数乘法指令,和8086功能用法一样,即指令中只给出一个操作,被乘数已默认,如果 指令给出的操作数是32位的话,被乘数默认为EAX,那么乘积将存放在EDX:EAX中,其中EDX存放高32位,EAX存放低32位,如果此时EDX= 0,即高32位为0的话,那么OF=0,CF=0,否则被置1。如果指令给出的操数作是16位的话,被乘数默认为AX那么乘积将放在DX:AX中,其中 DX中将存放高16位,AX中存放低16位。如果指令给出的操作数是8位的话,被乘数默认为AL,那么乘积将放在AX,AH中存放高8位,AL中存放低8 位。
b.DIV,无符号数的除法指令,和8086一样,指令给出一个操作数,被除数已默认。如果指令中给出的操作数为32,那么被除数将是EDX:EAX, 最终的商将存放在EAX, 余数将存放在EDX中。如果指令给出操作数为16位,那么被除数为EAX,最终得到的商放在AX,余数放在EAX的高16位。如果指令中给出的操作数为8位,那么被除数是16位,最终得到的商将放在AL中,余数放在AH中。
c.IMUL,有符号数的乘法指令,除了具有8086的用法外,有新的形式:
c1.IMUL DST,SRC;将源操作数SRC与目的操作DST相乘,并将结果送往DST。
c2.IMUL DST,SRC1,SRC2;将源操作数SRC1与源操作数SRC2相乘,并将结果送往DST。
使用这种形式必须遵守的规则,形式c1指令中目的操作数必须是16位或32位通寄存器,源操作数的长度 必须与目的操作的长度一样(8位立即数除外,即00H-FFH或80H-7FH),源操作数可以是通用寄存器,也可以是存储单元或立即数。形式c2指令中 的源操作数SRC1可以是通用寄存器也可以是存储单元,源操作数SRC2必须是立即数,DST必须是16位或32位通用寄存器。呵呵,对于这些规则无需去 问为什么,这是硬件的特性决定的,如果一定要问为什么,那只能问INTEL公司的硬件工程师了:)。同时,有一点要注意的是:这两种形式的指令,目的寄存 器的长度与源操作数长度一样(8位立即数除外),这样的话,该指令事实上对有符号数和无符号数是一样的,因为乘积的低位部分均存储在目的寄存器中,而高位 部分在这两种形式的指令中不予以存储。
d.IDIV,有符号数的除法指令,用法和8086相同,不过支持32位操作。

C.符号扩展指令CBW,CWD,CWDE,CDQ
a.CBW,前面已介绍,在第三篇。
b.CWD,前面已介绍,在第三篇。
c.CWDE,是80386新增的指令。格式:CWDE。功能:将AX的符号位扩展到EAX的高16位中。
d.CDQ,是80386新增的指令。格式:CDQ。功能,将EAX的符号位扩展到EDX中。
e.以上四条指令均不影响标志位。
f.举例说明:
;If AX=1234H,EAX=99991234H
CBW;After processing the instruction,AX=1234,DX=0000H
CDQ;After processing the instruction,EAX=99991234H,EDX=FFFFFFFFH

BB.逻辑运算指令和移位指令NOT,AND,OR,XOR,TEST,SAL,SAR,SHL,SHR,ROL,ROR,RCL,RCR,SHLD,SHRD
a.NOT,AND,OR,XOR,TEST这些指令的功能和用法与8086完全相同,不过它们支持32位操作。
b.TEST,测试指令,该指令测试的结果并不回送到目的操作数和源操数。之所以要使用这条的指令,主要是因为根据TEST指令得到的结果,进行程序的条件转移。
c.SAL,算术左移,功能和8086一样,但在8086中,如果在移位的位数超过1位,那么一定要移 位的位数放在CX寄存器中。在80386中,可以不用这样做,其它的移位指令也一样。除了这一点以外,用法和8086一样,当然也支持32位操作。以下的 语句均是合法的。
SHL AL,5;这在8086中是非法,但在80386中是合法的
SHL WORD PTR [SI],3
d.SAR,算术右移,将操作数右移指定的位数,但左边的符号位保持不变,移出的最低位进入CF标志位。
e.SHL,逻辑左移,用法和功能与SAL一样。
f.SHR,逻辑右移,将操作右移指定的位数,同时每移一位,左边用0补充,移出的最低位进入CF标志位。
g.说明:在80386中,实际移位的位数是指令中移位位数的低5位,也就是说移位位数的范围在0- 31或0-1FH,CF标志位总是保留着目的操作数最后被移出位的值。当移位位数大于操作数的长度时,CF被置0。如果移位位数为1,移位前后的结果的符 号位都是一样,那么很明显的是该操作数经移位后没有移出,此时OF=0。这四条指令的移位示意图(我画的是16位操作数的移位示意图,8位和32依此类 推),SAL,SHL相当于乘法;SAR,SHR相当于除法。
SAL:
|-------------------------------------------------------------------------------------------|
|CF|<-|bit15|bit14|bit13|bit12|bit11|bit10|bit9|bit8|bit7|bit6|bit5|bit4|bit3|bit2|bit1|bit0|
|-- ----------------------------------------------------------------------------------------|
SHL:
|-------------------------------------------------------------------------------------------|
|CF|<-|bit15|bit14|bit13|bit12|bit11|bit10|bit9|bit8|bit7|bit6|bit5|bit4|bit3|bit2|bit1|bit0|
|--- ---------------------------------------------------------------------------------------|
SAR:
|--------------------------------------------------------------------------------------------|
|-|bit15|bit14|bit13|bit12|bit11|bit10|bit9|bit8|bit7|bit6|bit5|bit4|bit3|bit2|bit1|bit0|->|CF||
| |---|----------------------------------------------------------------------------------------|
| ^
|-----|最高位保持不变
SHR:
|--------------------------------------------------------------------------------------------|
0->|bit15|bit14|bit13|bit12|bit11|bit10|bit9|bit8|bit7|bit6|bit5|bit4|bit3|bit2|bit1|bit0|->|CF||
|--------------------------------------------------------------------------------------------|
h.ROL,循环左移,支持32位操作数,用法和8086一样。
i.ROR,循环右移,支持32位操作数,用法和8086一样。
j.RCL,带进位的循环左移,支持32位操作数,用法和8086一样。
k.RCR,带进位的循环右移,支持32位操作数,用法和8086一样。
l.ROL,ROR,RCL,RCR的移位示意图(仍然以16位操作数来画,8位/32位依次类推):
ROL:
|--------------------------------------------------------------------------------------------------|
|<-|bit15|bit14|bit13|bit12|bit11|bit10|bit9|bit
8|bit7|bit6|bit5|bit4|bit3|bit2|bit1|bit0|<--------|
|--------------------------------------------------------------------------------------------------|
|--------------------------------------------------------------------------------------------------|
ROR:
|-------------------------------------------------------------------------------------------|
|->|bit15|bit14|bit13|bit12|bit11|bit10|bit9|bit8|bit7|bit6|bit5|bit4|bit3|bit2|bit1|bit0|->|
|-------------------------------------------------------------------------------------------|
|-------------------------------------------------------------------------------------------|
RCL:
|-------------------------------------------------------------------------------------------------|
|<-|CF|<-|bit15|bit14|bit13|bit12|bit11|bit10|bit9|bit8|bit7|bit6|bit5|bit4|bit3|bit2|bit1|bit0|<-|
|-------------------------------------------------------------------------------------------------|
|-------------------------------------------------------------------------------------------------|
RCR:
|-------------------------------------------------------------------------------------------------|
|->|CF||
|---- --------------------------------------------------------------------------------------------|
|-------------------------------------------------------------------------------------------------|
m.SHLD,80386新增的双精度左位指令,指令格式:SHLD OPRD1,OPRD2,M
n.SHRD,80386新增的双精度右移指令,指令格式:SHRD,OPRD1,OPRD2,M
o.m,n这两条指令的使用规则是:源操作数OPRD1可以是16位或32位通用寄存器或者16位存储 单元或者32位存储单元,源操作数OPRD2必须是16位或32位通寄存器,M表示移位次数,可以是CL寄存器,也可以是8位立即数。功能:SHLD是将 源操作数OPRD1移M位,空出的位用OPRD2高端的M位来填补,源操作数OPRD2的内容不变,最后移出的位放在CF中;SHRD将源操作数 OPRD1移M位,空出的位用OPRD2低端M位来填补,源操作数OPRD2保持不变,最后移出的位放在CF中,对于这两条指令,当移位位数仅为1的话, 移出和移后的符号位不变的话,那么OF=0,如果符号位不一样的话,那OF=1。
p.这两条指令是80386新增的指令,举两个简单的例子加以说明:
p1.SHLD:
MOV AX,8321H
MOV DX,5678H
SHLD AX,DX,1
SHLD AX,DX,2
分析一下该指令的详细执行过程(用示意图, 第一个图画的就是AX的内容):
AX=8321h
|-------------------------------|
|1|0|0|0|0|0|1|1|0|0|1|0|0|0|0|1|
|-------------------------------|
根据指令SHLD AX,DX,1,先左移一位,得到AX=0642H:
|-------------------------------|
|0|0|0|0|0|1|1|0|0|1|0|0|0|0|1|0| CF=1
|-------------------------------|
经过上一步的移位后,AX的最后一位(即bit0)空出来,其值为0;根据指令的用法将用DX的第15位填充,填充后AX的内容为:
|-------------------------------|
|0|0|0|0|0|1|1|0|0|1|0|0|0|0|1|0|
|-------------------------------|
同时由于移位后AX的符号位与移位前AX的符号位不同,所以在移位过程中产生了溢出,OF=1,最后结果AX=0642H。
同理,SHLD AX,DX,2,执行完这条指令后,最后结果为AX=0644H
p2.SHRD:
MOV EAX,12345678H
MOV EDX,99994599H
SHRD AX,DX,1
SHRD AX,DX,2
分析一下该指令的详细执行过程(用示意图,第一个图画的是EAX的内容):
EAX=12345678H
|---------------------------------------------------------------|
|0|0|0|1|0|0|1|0|0|0|1|1|0|1|0|0|0|1|0|1|0|1|1|0|0|1|1|1|1|0|0|0|
|---------------------------------------------------------------|
根据指令SHRD AX,DX,1,将AX右移一位得到EAX=091A2B3EH:
|---------------------------------------------------------------|
|0|0|0|0|1|0|0|1|0|0|0|1|1|0|1|0|0|0|1|0|1|0|1|1|0|0|1|1|1|1|1|0|
|---------------------------------------------------------------|
经过上一步的移位后,EAX的最高位(第31位)空出来用0填充,根据指令的用法,最EDX的第0位来填充,填充后EAX的内容为:
|---------------------------------------------------------------|
|1|0|0|0|1|0|0|1|0|0|0|1|1|0|1|0|0|0|1|0|0|1|1|0|0|1|1|1|1|0|0|0|
|---------------------------------------------------------------|
即EAX=891A2B3EH,CF=0,OF=0
同理,指令SHRD AX,DX,2,执行完这条件指令后,最后结果为EAX=048D159C,CF=0,OF=0

80386 ASM程序设计基础– 80386处理器的寻址方式

80386处理器的寻址方式
在实式模式下,80386处理器的最大寻址空间仍然为1M,和8086/8088相似。即段地址*10H+段内偏移地址,从而形成20位地址。此种模式下,段基址是16的倍数,长度最大不超过64K。
在保护模式下,80386处理器可以使用所有的物理内存。段基址可以是32位,也可以不是16的倍数, 同时它的最大长度为4G,这与8086完全不同,在形成逻辑地址时用段基址直接加上段内偏移地址,而并不将段基址左移4位(乘以16)。通常情况下,除了 访问堆栈外,默认的段都为DS,有跨段前缀就另当别论了。在以BP,EBP,ESP作为基址寄存器时,这时默认的段寄存器应该是SS,举几个简单的例子:
MOV EAX,[SI];这里的段寄存器是DS
MOV EAX,FS:[ESI];这里的段寄存器是FS,因为指令中使用跨段前缀显示指定了
MOV EAX,[BP];这里的段寄存器是SS,因为指令中使用了BP作为基址寄存器
MOV EAX,GS:[BP];这里段寄存器是GS,因为指令中使用跨段前缀显示指定了
80386中32位数的操作的顺序是“高高低低”,即是说高16-》高16,高8-》高8,低16-》 低16,低8-》低8,这和8086相似。同时80386微处理器兼容所有8086的寻址方式,而且对8086的寻址方式有很大的改进和扩展。在8086 下,只允许BP,BX,SI,DI作为寻址寄存器,但在80386下,8个通用寄存器都可以作为寻址寄存器。不过有一点要注意的是在基址变址寄存器寻址方 式或相对基址变址寻址方式中,段寄存器由基址寄存器来确定,而不是由变址寄存器来确定,同时除ESP外其它的7个通用寄存器都可以作为变址寄存器,用代码 来表示就是:
MOV EAX,[EBP+ESP+2];这条指令是错误的,因为不可以用ESP作为变址寄存器
MOV EAX,[EBP+ESI+10H];这里的段寄存器应该有基址寄存器来决定。基址寄存器是BP,那么这里的段寄存就是SS
MOV EAX,GS:[EBP+EDI+100H];不用看了,这里的段寄存器应该是GS,因为指令通过跨段前缀显示指定了
80386支持的基地址+变址+位移量寻址进一步满足了高级语言支持的数据类型。对于C语言来讲,普通变量,数组,结构体,结构体的数组,数组的构体我们既可存放在栈中(静态定义-static definition),也可以存放在堆中(动态定义-dynamic definition),用ASM也一样可以实现。基址变址寄存器提供了两个可以改变的部分,而位移量则是静态的。看下面的例子:
//Variables in C Programming-Language,the corresponding ASM will list below
void main()
{
int a;//普通的变量,用ASM寻址时直接用DS:[一位移量],如DS:[2000],属于直接寻址方式
int array[24];//数组,用ASM寻址时用DS:[BX+SI*4],4表示整型的长度,属于基址变址寻址方式
struct abc
{
int a,b,c;
float d;
};
struct abc aa;//结构体,用ASM寻址时DS:[BX+Shift],Shift代表位移量,属于寄存器相对寻址方式
struct abc aa[100];//结构体数组,用ASM寻址时用DS:[BX+SI*sizeof(abc)+Shift],属于相对基址变址寻址方式
struct cde
{
int array[100];
float e,f,g;
};
struct cde ccc;//数组结构体,用ASM寻址时用DS:[BX+SI*4+Shift],属于相对基址变址寻址方式
}
80386与8086的寻址方式差不多完全一样,只不过80386的寻址方式更灵活,它的操作数有32位,16位,8位。
让我们再重温一下8086的寻址方式:
a.立即寻址,所谓立即寻址就是操作数就在指令中,比如说:MOV AX,5678H
b.直接寻址,即直接包含操作数的有效地址EA,比如说MOV AX,[1234]
c.寄存器间址寻址,用寄存器的内容来作为操作数的有效地址,比如说SI=1234,MOV AX,[SI],8086下可用的寄存器只有4个:BX,BP,SI,DI,80386下8个通用的寄存器都可以使用。
d.寄存器相对寻址,即在寄存器间址寻址方式的基础上再加一个位移量,位移量可以是8位也可以是16位,比如说MOV AX,[BX+90H]。
e.基址变址寻址,即操作数的有效地址由一基址寄存器和一变址寄存器产生,如MOV AX,[BX+SI]。那么在8086下,只有SI,DI可以作为变址寄存器,在80386下除ESP外的其它7个通用寄存器都可以作为变址寄存器,比如说MOV AX,[BX+SI]。
f.相对基址变址寻址,在e寻址方式的基础上加上一位移量,比如说MOV AX,[BX+SI+100H]。
在8086下,我们如进行字节或字操作,往往要加上伪指令WORD PTR或BYTE PTR。在80386下不用显示指定,处理器会自动处理,当发现目的操作为8位时,处理器就会进行8位操作,同理当发现目的操作为16位,处理器就会进行16位操作,80386下以目的操作数的长度为准,以下几条简单的传送指令:
MOV AL,CS:[EAX];8位操作,段寄存器是CS,寻址方式是寄存器间址寻址
MOV AL,ES:[BX];8位操作,段寄存器是ES,寻址方式是寄存器间址寻址
MOV EDX,[EDX+EBX+1234H];32位操作,段寄存器是DS,寻址方式是相对基址变址寻址
MOV AX,[EBX+ESI*4];16位操作,段寄存器是DS,寻址方式是基址变址寻址
MOV BH,ES:[EBX+EDI+900H];8位操作,段寄存器是ES,寻址方式是相对基址变址寻址
MOV DL,[EBP+ESI+1900H];8位操作,段寄存是SS,因为用了EBP作为基址寄存器。寻址方式是相对基址变址寻址

80386 ASM程序设计基础

80386 ASM程序设计基础,呵呵,这是最近一段时间我的业余爱好。本期将连续推出若干篇有关80386ASM程序设计的基础,主要介绍80386ASM指令的详 细用法及如何在80386实模式下,保护模式下及虚拟8086模式编程以及我会详细介绍80386下的段页管理机制,我会将80386下的指令与8086 下的相同指令进行比较。在你去看罗云彬的ASM编程之前,不妨先看看我的基础篇,希望有志于从事汇编语言的朋友,多提意见。
80386处理器是Intel公司80x86发展史上的里程碑,它不但兼容先前的8086/8088,80186,80286处理器,而且也为后来的486,Pentium(586),Pentium Pro(686)的发展打下了坚实的基础,对于我们程序员来讲更重要的是:我们关心80386在指令上到底有哪些扩展呢?80386有哪些寻址方式呢?毫无疑问,它不但兼容了8086的所有指令,而且还对它们进行增强.
呵呵,我知道有很多人问我CPU已经发展到PentiumIIII,没有必要学习80386的汇编。其实不然, 80386处理器中的保护模式,虚拟8086模式以及地址的段页管理机制,虚拟内存这些都是以后处理器的核心。所以说80386是后续发展处理器的基础, 比如说80486实质上80386+80387协处理,这块协处理器主要用于处理浮点运算,Pentium处理器在80386指令的基础上增加了57条指 令,8个数据类型,8个64位的寄存器来处理多媒体。从这一点来看,完全有必要了解80386ASM,这就好像学习80386,必须先要熟练掌握 8086。
1.80386的的寄存器:
80386的寄存器可以分为8组:通用寄存器,段寄存器,指令指针寄存器,标志寄存器,系统地址寄存器,控制寄存器,调试寄存器,测试寄存器,它们的宽度都是32位的。本篇主要介绍80386的寄存器。
A1.General Register(通用寄存器)
EAX,EBX,ECX,EDX,ESI,EDI,ESP,EBP,它们的低16位就是8086的AX,BX,CX,DX,SI,DI,SP,BP,它们的含义如下:
EAX:累加器
EBX:基址寄存器
ECX:计数器
EDX:数据寄存器
ESI:源地址指针寄存器
EDI:目的地址指针寄存器
EBP:基址指针寄存器
ESP:堆栈指针寄存器
这些寄存器可以将低16位单独存取,也就是8086的AX,BX,CX,DX,SI,DI,SP,BP,在存取这些寄存器的低16位(AX,BX,CX,DX,SI,DI,SP,BP),它
们的高16位不受影响,同时和8086一样对于AX,BX,CX,DX这四个寄存器来讲,可以单独存取它们的高8位和低8位(AH,AL,BH,BL,CH,CL,DH,DL)

A2:Segment Register(段寄存器)
除了8086的4个段外(CS,DS,ES,SS),80386还增加了两个段FS,GS,这些段寄存器都是16位的,它们的含义如下:
CS:代码段(Code Segment)
DS:数据段(Data Segment)
ES:附加数据段(Extra Segment)
SS:堆栈段(Stack Segment)
FS:附加段
GS 附加段

A3:Instruction Pointer(指令指针寄存器)
EIP,它的低16位就是8086的IP,它存储的是下一条要执行指令的地址。

A4:Flag Register(标志寄存器)
EFLAGS,和8086的16位标志寄存器相比,增加了4个控制位,不过这4个控制位它们在实模下不起作,这四个控制位分别是:
a.IOPL(I/O Privilege Level),I/O特权级字段,它的宽度为2bit,它指定了I/O指令的特权级。如果当前的特权级别在数值上小于或等于IOPL,那么I/O指令可执行。否则,将发生一个保护性异常。
b.NT(Nested Task):控制中断返回指令IRET,它宽度为1位。NT=0,用堆栈中保存的值恢复EFLAGS,CS和EIP从而实现中断返回;NT=1,则通过任务切换实现中断返回。
c.RF(Restart Flag):重启标志,它的宽度是1位。它主要控制是否接受调试故障。RF=0接受,RF=1忽略。如果你的程序每一条指令都被成功执行,那么RF会被清0。而当接受到一个非调试故障时,处理器置RF=1。
d.VM(Virtual Machine):虚拟8086模式(用软件来模拟8086的模式,所以也称虚拟机)。VM=0,处理器工作在一般的保护模式下;VM=1,工作在V8086模式下。
其它16个标志位的含义和8086一样,在这里也重温一遍:
e.CF(Carry Flag):进位标志位,由CLC,STC两标志位来控制
f.PF(Parity Flag):奇偶标志位
g.AF(Assistant Flag):辅助进位标志位
h.ZF(Zero Flag):零标志位
i.SF(Singal Flag):符号标志位
j.IF(Interrupt Flag):中断允许标志位,由CLI,STI两条指令来控制
k.DF(Direction Flag):向量标志位,由CLD,STD两条指令来控制
l.OF(Overflow Flag):溢出标志位。
控制寄存器,系统地址的寄存器,调试寄存器,测试寄存器将在介绍完80386分段,分页管理机制后介绍,请继续关注第二篇“80386存储器的寻址方式”。

VI 常用命令(不断添加中)

首先记住基础的三条:

一、VI有三种模式:『一般模式』、『编辑模式』与『指令列命令模式』

二、经常使用的按键:
命令 移动
h 左一个字符
j 下一行
k 上一行
l 右一个字符
w, W 前一个单词 (W 忽略标点)
b, B 后一个单词 (B 忽略标点)
$ 到行尾
^ 到行首第一个非空字符
0 行首
G 到缓冲首
nG 到第 n


三、常用命令:



x--删除一个字符 删除文字的最简单的方式是用 x。这个命令的结果是光标所处 的字符的消失,后面的文字左移动。如果你删除的字符是一行最后的一个字符, 那么光标将向左移动一个,这样光标就不会停留在不存在的字符的下面了。假如 没有任何文字了,喇叭就叫。

d--删除对象 这个命令的右边还要有一定的文字对象。文字对象就是一块文字。他右边接的就是在控制光标移动的那些字符。例如 w 表示向前一个单词,那么 dw 将删除下一个单词。5w 表示前进 5 个单词,那么 d5w 将删除他们。

dd--删除一行 最常用的 d 系列命令之一。和前面一样, 5dd 将删除 5 行

D--整个删除 大写形式的 D 用来删除从光标到行尾。和 d$ 一样效果。

u--恢复 要后悔吗?他不仅仅撤消删除,还撤消你所有的编辑工作。

.--重复 重复编辑工作。

:%s/oldword/newword/g

将文中所有的oldword替换为newword

2005年4月5日星期二

JUnit测试程序编写规范

一、 程序命名规范

1.测试类的命名

测试类的命名规则是:被测试类的类名+Test

比如有一个类叫IrgSrhDelegate,那么它的测试类的命名就是IrgSrhDelegateTest

2.测试用例的命名

测试用例的命名规则是:test+用例方法名称

比如要测试的方法叫updateData,那么测试用例的命名就是testUpdateData

(说明:"用例方法"就是指被测试的类中所包含的方法,而"测试用例"就是指测试类中所包含的方法)

比如IrgSrhDelegate中有一个方法叫做findByIrgFindParam,那么在IrgSrhDelegateTest中对应的测试用例名称就是testFindByIrgFindParam。

3.其它命名规范

本规范未说明的其它命名规范请参照《JAVA语言编码规范》(ENO-W063-JAVA Coding Rule.doc)。

二、 测试程序的包名定义规范

为了保持测试程序的独立和稳定性,请按照下面的方式组织测试程序:

假如被测试类的包名是com.wistrons.util,那么测试类的包名就是test.com.wistrons.util。也就是说在被测试类的包名前加上"test.",这就是测试类的包名。

三、 测试数据的准备方案

准备测试数据时有三种方案可以选择。

1.在程序中直接写入测试数据

在要输入的数据项不多的情况下可以采用这种方式

2.使用junitpack包中的InputDataUtil工具类

(要使用这个工具,请在测试程序中加上import junitpack.InputDataUtil

这种方法要求把测试数据写在一个XML文件中,XML的格式如下所示:

h0001

...

...

在这个XML文件中的根节点为inputs,根节点下可以有多个input节点。每个input节点代表一个case中需要的所有数据。

使用这个工具类的操作步骤如下:

1) 获取XML的存放路径。

请把写好的XML存放在测试类所在的目录中,然后可以按如下方式取得XML的存放路径:

String xml = IrgSrhDelegateTest.class.getResource(".").toString() +"test.xml";

2) 创建InputDataUtil的实例。

InputDataUtil inputUtil = new InputDataUtil();

3) 在InputDataUtil实例中设置接受数据的类名,InputDataUtil将此类与XML进行数据绑定。

inputUtil.setClassName("jp.co.liondor.common.fz25IrgSrh. SeekIrgSrhOpt");
4) 调用InputDataUtil.parse()方法,从XML中采集数据
java.util.Vector vector = (java.util.Vector) inputUtil.parse(xml);

5) 从Vector中取出被绑定类的实例

for (int i = 0; i

SeekIrgSrhOpt opt = (SeekIrgSrhOpt) vector.get(i);

...
}

现在对InputDataUtil的工作原理进行说明。InputDataUtil会根据input节点下的子节点名来设置被绑定的类中对应的 set方法,然后把XML中的数据设置到被绑定类中。比如上例XML中,input节点下有三个子节点:IrgCd、IrgName、IrgKname。 那么在调用InputDataUtil.parse()方法时,InputDataUtil就会分别调用SeekIrgSrhOpt类的setIrgCd ()、setIrgName()、setIrgKname()方法,把数据设置到SeekIrgSrhOpt的实例类中,并返回包含这些实例类的 Vector对象。

3.使用Digester

(要使用这个工具,请在测试程序中加上import org.apache.commons.digester.Digester

Digester是Apache提供的一个工具类,上面的InputDataUtil也是从Digester类继承的。当使用InputDataUtil暂时无法解决的时候,可以直接使用Digester。

使用Digester的步骤如下:

1) 获取XML的存放路径。

获取方式与使用InputDataUtil相同。

2) 创建Digester的实例。

Digester dig = new Digester();

3) 设定与inputs节点绑定的类为Vector

dig.addObjectCreate("inputs", "java.util.Vector");

4) 设定与input节点绑定的类

dig.addObjectCreate("inputs/input",

" jp.co.liondor.common.fz25IrgSrh. SeekIrgSrhOpt ");

5) 根据input节点下的子节点,依次设定相应的set方法

dig.addCallMethod("inputs/input/ IrgCd ", "setIrgCd ", 1);

dig.addCallParam("inputs/input/ IrgCd ", 0);

6) 设定向Vector中加入数据的方法

dig.addSetNext("inputs/input", "add");

7) 调用Digester.parse()方法,从XML中采集数据

java.util.Vector vector = (java.util.Vector) dig.parse(xml);

8) 从Vector中取出被绑定类的实例

for (int i = 0; i

SeekIrgSrhOpt opt = (SeekIrgSrhOpt) vector.get(i);

...

}

Digester的用法非常灵活,可以组织非常复杂的数据。

关于Digester的详细用法请参考http://jakarta.apache.org/commons/digester/

四、 对UI测试的原则

对UI做单元测试必须做到不能牵涉到业务逻辑操作(比如数据库操作、与Server的交互)。否则就是UI的设计不合理。对UI的单元测试应该非常单纯,就只是测试界面的动作是否符合设计要求。

五、 测试数据的覆盖率
测试时所准备的测试数据要覆盖程序中所有可能出现的CASE。
六、 测试记录

记录测试的过程和结果,请使用Log4j工具。

七、 测试粒度

选择测试粒度的原则:

1) 被测试类中所有public、protected方法都要测到。

2) 对于简单的set和get方法没有必要做测试。

八、 附录:参考文档一览

Digester文档 http://jakarta.apache.org/commons/digester/

JAVA语言编码规范 ENO-W063-JAVA Coding Rule.doc

JUnit官方网站 http://www.junit.org/

◆ 如何修改动态库符号表

◆ 如何修改动态库符号表

作者:wangdb (mailto:wangdb@nsfocus.com)
主页:http://www.nsfocus.com/
日期:2000-10-14

一、ELF 文件和有关术语

Unix 系统的可执行文件和动态库文件是以 ELF 格式存放的。为使下面的叙述
清晰而没有伎义,先简要介绍一下 ELF 文件格式,并约定一些术语。关于ELF
文件格式的详细情况请参看有关文献。

ELF 文件中代码、连接信息和注释是以节(section)为单位存放的,并存有一
个节头表(section header)。对每一节,在节头表中都有一个表项(节头表项)
与之对应,表项记录了该节的一些信息,例如该节在文件中的位置信息和该节
的字节长度信息。

程序运行读入内存时,是以程序段(program segment)为单位读入的。在 ELF
文件中有一个程序头表(program header table),每个程序段在程序头表中有
一个表项(程序头表项)与之对应,表项记录了该程序段的有关信息,例如该程
序段在文件中的位置信息和该程序段的字节长度信息。程序段的内容由若干节
组成,节的内容组合在一起连成一片构成程序段的内容。

在所有这些节中,有一节的内容由字符串构成,这些字符串是各节的名称,叫
节名。下面称这一节为节名表。另有一节,节名为".dynsym",它的内容为符
号表,符号表的每一表项记录了一个符号的有关信息,例如该符号对应的代码
的地址值。还有一节,节名为".dynstr",它的内容由字符串构成。大多数符
号在该节中有一个字符串与之对应,这个字符串是该符号的符号名。而每一函
数对应一个符号,函数名即为符号名。下面称被某一函数对应的符号为函数符
号。

ELF 文件开始处的一段叫 ELF 文件头。它记录了程序头表在文件中的偏移、
程序头表的表项数目、程序头表每一表项的字节长度、节头表在文件中的偏移、
节头表的表项数目、节头表每个表项的字节长度。它还记录了节名表所在的节
的索引序号。

二、动态库符号表修改方法

修改动态库符号表的方法和步骤如下:

第一步:

读取 ELF 文件头,取出

(1) 程序头表在文件中的偏移,获取程序头表在文件中的位置;
(2) 程序头表的表项数目和程序头表每一表项的字节长度;
(3) 节头表在文件中的偏移,获取节头表在文件中的位置;
(4) 节头表的表项数目和节头表每个表项的字节长度;
(3) 节名表所在的节的索引序号。

ELF 文件头在文件中的偏移为零,即起始于 ELF 文件开头的第一字节,它的
数据结构为:

#define EI_NIDENT (16)

typedef uint16_t Elf32_Half;
typedef uint32_t Elf32_Word;
typedef uint32_t Elf32_Addr;
typedef uint32_t Elf32_Off;
typedef uint16_t Elf32_Section;

typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number及其他信息 */
Elf32_Half e_type; /* ELF 文件类型 */
Elf32_Half e_machine; /* 机器型号 */
Elf32_Word e_version; /* 版本 */
Elf32_Addr e_entry; /* 程序入口虚地址 */
Elf32_Off e_phoff; /* 程序头表在文件中的偏移 */
Elf32_Off e_shoff; /* 节头表在文件中的偏移 */
Elf32_Word e_flags; /* 处理器标志 */
Elf32_Half e_ehsize; /* ELF 文件头长度 */
Elf32_Half e_phentsize; /* 程序头表每个表项长度 */
Elf32_Half e_phnum; /* 程序头表的表项总数 */
Elf32_Half e_shentsize; /* 节头表每个表项长度 */
Elf32_Half e_shnum; /* 节头表的表项总数 */
Elf32_Half e_shstrndx; /* 节名表所在的节的表项索引号 */
} Elf32_Ehdr;

第二步:

依据节头表在文件中的偏移和节名表所在的节的索引序号,定出节名表在节头
表中对应的表项的文件偏移,即定出该表项在文件中的位置。读取该表项,取
出节名表在文件中的偏移和该节在文件中的字节长度。节头表由若干表项组成,
每个表项的内容按下面的数据结构来组织:

typedef struct
{
Elf32_Word sh_name; /* 节名索引号 */
Elf32_Word sh_type; /* 节类型 */
Elf32_Word sh_flags; /* 节标志 */
Elf32_Addr sh_addr; /* 执行时该节虚地址 */
Elf32_Off sh_offset; /* 在文件中的偏移 */
Elf32_Word sh_size; /* 节长度 */
Elf32_Word sh_link; /* 到其他节的连接 */
Elf32_Word sh_info; /* 其他信息 */
Elf32_Word sh_addralign; /* alignment */
Elf32_Word sh_entsize; /* 如内容为表,每个表项的长度 */
} Elf32_Shdr;

第三步:

按节名表在文件中的偏移和该节的长度读取节名表,并缓存在一个缓冲区中。

第四步:

依据节头表在文件中的偏移、节头表的表项总数、以及节头表每个表项长度搜
索节头表。对每个节头表项,读出节名索引号,由节名索引号从上面缓存在缓
冲区中节名表得出该节头表项对应的节的名字。如果名字为".dynsym",记录
该节的在文件中的偏移和字节长度。名字为 ".dynsym" 的节的内容是符号表,
除了记录它在文件中的偏移和字节长度外,还要记下它的每个表项的长度。每
个表项即是对一个符号所记录的信息,表项的数据结构为:

typedef struct
{
Elf32_Word st_name; /* 符号名索引号 */
Elf32_Addr st_value; /* 符号地址值 */
Elf32_Word st_size; /* 符号对应的代码长度 */
unsigned char st_info; /* 符号类型和梆定信息 */
unsigned char st_other; /* 未用,值为 0 */
Elf32_Section st_shndx; /* 所在节的节索引号 */
} Elf32_Sym;

在搜索节头表时,除了找 ".dynsym" 节外,还要找到名字为 ".dynstr" 的节,
记下它的在文件中的偏移和字节长度。由该结在文件中的偏移和字节长度读取
它的内容,并缓存在一个缓冲区中。

第五步:

按第四步中得到的 ..dynsym节的字节长度和符号表的表项的长度算出符号表表
项数目,也即符号的数目。然后依据第四步中得到的符号表(即.dynsym节)在
文件中的偏移把文件指针打到符号表所在的位置,检索符号表找到要修改的符
号。方法是从符号表表项中读出符号名索引号(st_name)的值,这个值即是该
表项记录其信息的符号的符号名字符串在 .dynstr 节中的偏移,由这个值在
第四步中缓存 .dynstr 节的缓冲区中取出符号名,把符号名和要找的符号的
符号名进行比较。

第六步:

第五步中找到了要修改的符号,现在可以进行修改了。所谓修改符号就是修改
该符号在符号表(.dynsym 节)中表项,因为表项的内容是对该符号的有关信
息的记录。hook 时需要关注的是符号地址值(st_value)和符号对应的代码长
度(st_size)。可以把符号地址值(st_value)和符号对应的代码长度(st_size)
修改为动态库中另一符号的相应的 st_value 和 st_size 值。通常修改的是
函数符号。如果是函数符号
,那么修改后当调用该函数时,实际调用的是上面
修改时取其 st_value 和 st_size 值的另一符号所对应的函数。也可以向ELF
文件中加入的一段几十字节的 shellcode 或其他代码,修改符号表时把所修
改的符号在符号表中的表项的 st_value 值指向这段 shellcode 或其他代码,
st_size 的值置为这段代码的字节长度。那么,程序调用被修改的符号所对应
的函数时,实际调用的是这段 shellcode 或其他代码。

三、示例程序

为对上面所说的进行解释,下面的给出一个示例程序。它打印出 ELF 文件的
有关信息,然后通过修改符号表把动态库的函数1 hook 到函数2。这样做以后,
如果某程序编译时与该动态库连接了,它调用函数1时,程序运行时实际调用
的是函数2。函数2可以是动态库本身就有的函数,也可以是你向 ELF 文件中
偷偷加入的一段几十字节的 shellcode。这是向系统置入后门的一个方法,特
别是 hook 经常被调用的动态库。

测试时做了一个小动态库,它仅有两个函数 haha() 和 huhu():
________________________________________________________
/* haha.c */

#include

void haha(void)
{
printf(" --- haha\n");
return;
}
________________________________________________________
/* huhu.c */

#include

void huhu(void)
{
printf(" --- huhu\n");
return;
}
________________________________________________________

[wangdb@redhat62 exploit]$ gcc -c -fPIC -O3 haha.c huhu.c
[wangdb@redhat62 exploit]$ gcc -shared haha.o huhu.o -o libtst.so.1.0
[wangdb@redhat62 exploit]$ ln -s libtst.so.1.0 libtst.so

程序 m.c 调用 huhu() 和 haha():
________________________________________________________
/* m.c */
int main()
{
haha();
huhu();
return 0;
}
________________________________________________________

[wangdb@redhat62 exploit]$ gcc m.c -L. -ltst -o ttt
[wangdb@redhat62 exploit]$ gcc -O3 hook_elf.c -o elf_hook
[wangdb@redhat62 exploit]$ ./ttt
--- haha
--- huhu
[wangdb@redhat62 exploit]$ ./elf_hook libtst.so huhu haha
.
.
.
[wangdb@redhat62 exploit]$ ./ttt
--- haha
--- haha
[wangdb@redhat62 exploit]$

下面是 hook_elf.c 程序:
________________________________________________________________________________
/*
* C Program File: hook_elf.c ---
*
* Description: This program read and print relevant information of ELF
* File, then hook function fun1 to fun2. After hooking, when
* some program call function fun1, actually it is fun2 being
* called.
* Usage:
* hook_elf
* (Note: when dst_sym == src_sym, ELF file is not changed.)
*
* Author: wangdb (wangdb@nsfocus.com)
*/

#include
#include
#include
#include
#include
#include
#include

#define EI_NIDENT (16)

typedef uint16_t Elf32_Half;
typedef uint32_t Elf32_Word;
typedef uint32_t Elf32_Addr;
typedef uint32_t Elf32_Off;
typedef uint16_t Elf32_Section;

/* 下面的数据结构定义取自 elf.h 头文件 */

/* The ELF file header. This appears at the start of every ELF file. */

typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} myElf32_Ehdr;

/* Program segment header. */

typedef struct
{
Elf32_Word p_type; /* Segment type */
Elf32_Off p_offset; /* Segment file offset */
Elf32_Addr p_vaddr; /* Segment virtual address */
Elf32_Addr p_paddr; /* Segment physical address */
Elf32_Word p_filesz; /* Segment size in file */
Elf32_Word p_memsz; /* Segment size in memory */
Elf32_Word p_flags; /* Segment flags */
Elf32_Word p_align; /* Segment alignment */
} myElf32_Phdr;

/* Section header. */

typedef struct
{
Elf32_Word sh_name; /* Section name (string tbl index) */
Elf32_Word sh_type; /* Section type */
Elf32_Word sh_flags; /* Section flags */
Elf32_Addr sh_addr; /* Section virtual addr at execution */
Elf32_Off sh_offset; /* Section file offset */
Elf32_Word sh_size; /* Section size in bytes */
Elf32_Word sh_link; /* Link to another section */
Elf32_Word sh_info; /* Additional section information */
Elf32_Word sh_addralign; /* Section alignment */
Elf32_Word sh_entsize; /* Entry size if section holds table */
} myElf32_Shdr;

/* Symbol table entry. */

typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* No defined meaning, 0 */
Elf32_Section st_shndx; /* Section index */
} myElf32_Sym;

/* The syminfo section if available contains additional information about
every dynamic symbol. */

typedef struct
{
Elf32_Half si_boundto; /* Direct bindings, symbol bound to */
Elf32_Half si_flags; /* Per symbol flags */
} myElf32_Syminfo;


/* Main routine */

int main(int argc, char *argv[])
{
myElf32_Ehdr *e_hdr_ptr;
myElf32_Phdr *p_hdr_ptr;
myElf32_Shdr *s_hdr_ptr;

myElf32_Sym *symptr;
myElf32_Syminfo *HashSymPtr;

int fd, i;
unsigned char buf[256];

unsigned int ProHdrFileOffset;
unsigned int SecHdrFileOffset;
unsigned int NamStrSecTblIndex;
unsigned int ProHdrTblEntrNum;
unsigned int SecHdrTblEntrNum;
unsigned int ProHdrTblEntrSize;
unsigned int SecHdrTblEntrSize;

unsigned int SecNamStrTblFileOffset = 0;
char SecNameStrTable[1024];
unsigned int SecNameIndex = 0;

unsigned char SymTblEntry[16];

unsigned int DebugInfoFileOffset = 0;
int DebugInfoSymTblNum = 0;
unsigned int DebugInfoStrTblFileOffset = 0;
char DebugInfoStrTable[4096];
unsigned int DebugInfoStrTblSize = 0;

unsigned int SymTblFileOffset = 0;
int SymTblNum = 0;
unsigned int SymNamStrTblFileOffset = 0;
char SymNamStrTable[2048];
unsigned int SymNamStrTblSize = 0;

unsigned int HashOffset =
0;
int HashTblNum = 0;

unsigned char src_sym[16], dst_sym[16];
unsigned char tmp_sym_addr[4];
unsigned char tmp_sym_size[4];
unsigned int src_sym_tbl = 0, dst_sym_tbl = 0;

if (argc \n", argv[0]);
exit(1);
}

if ( (fd = open(argv[1], O_RDONLY)) == -1 ) {
fprintf(stderr, "Can't open file \"%s\".\n", argv[1]);
exit(1);
}

fprintf(stdout, "Dump content of the ELF file '%s'\n", argv[1]);

fprintf(stdout, "Part I: ELF File Header...\n");

/* 读取 ELF 文件头 */
if ( read(fd, buf, 52) != 52 ) {
fprintf(stderr, "read error\n");
close(fd); exit(1);
}

e_hdr_ptr = (myElf32_Ehdr *)buf;

fprintf(stdout, "(Magic number and other info)e_ident: %s\n",
e_hdr_ptr->e_ident);
fprintf(stdout, "(Object file type)e_type: 0x%04X\n",
e_hdr_ptr->e_type);
fprintf(stdout, "(Architecture)e_machine: 0x%04X\n",
e_hdr_ptr->e_machine);
fprintf(stdout, "(Object file version)e_version: 0x%08X\n",
e_hdr_ptr->e_version);
fprintf(stdout, "(Entry point virtual address)e_entry: 0x%08X\n",
e_hdr_ptr->e_entry);
fprintf(stdout, "(Program header table file offset)e_phoff: 0x%08X\n",
e_hdr_ptr->e_phoff);
fprintf(stdout, "(Section header table file offset)e_shoff: 0x%08X\n",
e_hdr_ptr->e_shoff);
fprintf(stdout, "(Processor-specific flags)e_flags: 0x%08X\n",
e_hdr_ptr->e_flags);
fprintf(stdout, "(ELF header size in bytes)e_ehsize: 0x%04X\n",
e_hdr_ptr->e_ehsize);
fprintf(stdout, "(Program header table entry size)e_phentsize: 0x%04X\n",
e_hdr_ptr->e_phentsize);
fprintf(stdout, "(Program header table entry count)e_phnum: 0x%04X\n",
e_hdr_ptr->e_phnum);
fprintf(stdout, "(Section header table entry size)e_shentsize: 0x%04X\n",
e_hdr_ptr->e_shentsize);
fprintf(stdout, "(Section header table entry count)e_shnum: 0x%04X\n",
e_hdr_ptr->e_shnum);
fprintf(stdout, "(Section header string table index)e_shstrndx: 0x%04X\n",
e_hdr_ptr->e_shstrndx);

/* 记下程序头表在文件中的偏移、节头表在文件中的偏移、
节名表所在的节的索引序号、程序头表表项字节长度、程序头表表项数目、
节头表表项字节长度、节头表表项数目。*/
ProHdrFileOffset = (unsigned int)e_hdr_ptr->e_phoff;
SecHdrFileOffset = (unsigned int)e_hdr_ptr->e_shoff;
NamStrSecTblIndex = (unsigned int)e_hdr_ptr->e_shstrndx;
ProHdrTblEntrNum = (unsigned int)e_hdr_ptr->e_phnum;
SecHdrTblEntrNum = (unsigned int)e_hdr_ptr->e_shnum;
ProHdrTblEntrSize = (unsigned int)e_hdr_ptr->e_phentsize;
SecHdrTblEntrSize = (unsigned int)e_hdr_ptr->e_shentsize;

fprintf(stdout, "Part II: Program Header Table...\n");

if ( lseek(fd, (off_t)ProHdrFileOffset, SEEK_SET) != ProHdrFileOffset ) {
fprintf(stderr, "lseek to program header error.\n");
close(fd); exit(1);
}

for (i = 0; i p_type);
fprintf(stdout, "(Segment flags)p_flags: 0x%08X\n",
p_hdr_ptr->p_flags);
fprintf(stdout, "(Segment file offset)p_offset: 0x%08X\n",
p_hdr_ptr->p_offset);
fprintf(stdout, "(Segment virtual address)p_vaddr: 0x%08X\n",
p_hdr_ptr->p_vaddr);
fprintf(stdout, "(Segment physical address)p_paddr: 0x%08X\n",
p_hdr_ptr->p_paddr);
fprintf(stdout, "(Segment size in file)p_filesz: 0x%08X\n",
p_hdr_ptr->p_filesz);
fprintf(stdout, "(Segment size in memory)p_memsz: 0x%08X\n",
p_hdr_ptr->p_memsz);
fprintf(stdout, "(Segment alignment)p_align: 0x%08X\n",
p_hdr_ptr->p_align);
}

fprintf(stdout, "Part III: Section Header Table...\n");

/* 定出节名表所在的节在节头表中对应的表项的文件偏移。*/
SecNamStrTblFileOffset = SecHdrFileOffset + NamStrSecTblIndex * 40;
if ( lseek(fd, (off_t)SecNamStrTblFileOffset, SEEK_SET) !=
SecNamStrTblFileOffset || SecNamStrTblFileOffset == 0 ) {
fprintf(stderr,
"lseek to Section Table Entry for Section Name String Table error.\n");
close(fd); exit(1);
}
if ( read(fd, buf, (size_t)SecHdrTblEntrSize) != (ssize_t)SecHdrTblEntrSize ) {
fprintf(stderr, "read error\n");
close(fd); exit(1);
}
s_hdr_ptr = (myElf32_Shdr *)buf;
SecNamStrTblFileOffset = (unsigned int)s_hdr_ptr->sh_offset;

/* 读取节名表,并缓存在一个缓冲区中。*/
if ( lseek(fd, (off_t)SecNamStrTblFileOffset, SEEK_SET) !=
SecNamStrTblFileOffset || SecNamStrTblFileOffset == 0 ) {
fprintf(stderr, "lseek to Section Name String Table error.\n");
close(fd); exit(1);
}
if ( read(fd, SecNameStrTable, (size_t)s_hdr_ptr->sh_size) !=
(ssize_t)s_hdr_ptr->sh_size ) {
fprintf(stderr, "read error\n");
close(fd); exit(1);
}

if ( lseek(fd, (off_t)SecHdrFileOffset, SEEK_SET) != SecHdrFileOffset ||
SecHdrFileOffset == 0 ) {
fprintf(stderr, "lseek to section header error.\n");
close(fd); exit(1);
}

/* 记录符号表(即.dynsym节)在文件中的偏移,由它的字节长度和每个表项的
长度算出符号表的表项数目。同时记下.dynstr节在文件中的偏移和字节长度。*/
for (i = 0; i sh_type == 0x3 && s_hdr_ptr->sh_name == 0x11 ) {
SecNamStrTblFileOffset = (unsigned int)s_hdr_ptr->sh_offset;
}*/
if ( strcmp(SecNameStrTable + s_hdr_ptr->sh_name, ".symtab") == 0 ) {
DebugInfoFileOffset = (unsigned int)s_hdr_ptr->sh_offset;
DebugInfoSymTblNum = (int)((s_hdr_ptr->sh_size)/(s_hdr_ptr->sh_entsize));
}
if ( strcmp(SecNameStrTable + s_hdr_ptr->sh_name, ".strtab") == 0 ) {
DebugInfoStrTblFileOffset = (unsigned int)s_hdr_ptr->sh_offset;
DebugInfoStrTblSize = (unsigned int)s_hdr_ptr->sh_size;
}
if ( strcmp(SecNameStrTable + s_hdr_ptr->sh_name, ".dynsym") == 0 ) {
SymTblFileOffset = (unsigned int)s_hdr_ptr->sh_offset;
SymTblNum = (int)((s_hdr_ptr->sh_size)/(s_hdr_ptr->sh_entsize));
}
if ( strcmp(SecNameStrTable + s_hdr_ptr->sh_name, ".dynstr") == 0 ) {
SymNamStrTblFileOffset = (unsigned int)s_hdr_ptr->sh_offset;
SymNamStrTblSize = (unsigned int)s_hdr_ptr->sh_size;
}
if ( strcmp(SecNameStrTable + s_hdr_ptr->sh_name, ".hash") == 0 ) {
HashOffset = (unsigned int)s_hdr_ptr->sh_offset;
HashTblNum = (int)((s_hdr_ptr->sh_size)/(s_hdr_ptr->sh_entsize));
}
}

if ( lseek(fd, (off_t)SecHdrFileOffset, SEEK_SET) != SecHdrFileOffset ) {
fprintf(stderr, "lseek to section header error.\n");
close(fd); exit(1);
}

for (i = 0; i sh_name;
fprintf(stdout, "(Section name (string tbl index))sh_name: 0x%08X -> %s\n",
s_hdr_ptr->sh_name, SecNameStrTable + SecNameIndex);
fprintf(stdout, "(Section type)sh_type: 0x%08X\n",
s_hdr_ptr->sh_type);
fprintf(stdout, "(Section flags)sh_flags: 0x%08X\n",
s_hdr_ptr->sh_flags);<
br /> fprintf(stdout, "(Section virtual addr at execution)sh_addr: 0x%08X\n",
s_hdr_ptr->sh_addr);
fprintf(stdout, "(Section file offset)sh_offset: 0x%08X\n",
s_hdr_ptr->sh_offset);
fprintf(stdout, "(Section size in bytes)sh_size: 0x%08X\n",
s_hdr_ptr->sh_size);
fprintf(stdout, "(Link to another section)sh_link: 0x%08X\n",
s_hdr_ptr->sh_link);
fprintf(stdout, "(Additional section information)sh_info: 0x%08X\n",
s_hdr_ptr->sh_info);
fprintf(stdout, "(Section alignment)sh_addralign: 0x%08X\n",
s_hdr_ptr->sh_addralign);
fprintf(stdout, "(Entry size if section holds table)sh_entsize: 0x%08X\n",
s_hdr_ptr->sh_entsize);
}

fprintf(stdout,
"************************************************************************\n");

if ( lseek(fd, (off_t)DebugInfoStrTblFileOffset, SEEK_SET) !=
DebugInfoStrTblFileOffset || DebugInfoStrTblFileOffset == 0 ) {
fprintf(stderr, "lseek to Debug Info String Table error.\n");
goto next;
}
read(fd, DebugInfoStrTable, (size_t)(DebugInfoStrTblSize + 1));

if ( lseek(fd, (off_t)DebugInfoFileOffset, SEEK_SET) !=
DebugInfoFileOffset || DebugInfoFileOffset == 0 ) {
fprintf(stderr, "lseek to Debug Info Symbol Table error.\n");
goto next;
}
for (i = 0; i Symbol ID: %d\n", i);
fprintf(stdout, "Symbol_index_and_name: 0x%08X -> %s\n",
symptr->st_name, DebugInfoStrTable + symptr->st_name);
fprintf(stdout, "Symbol_value: 0x%08X\n", symptr->st_value);
fprintf(stdout, "Symbol_size: 0x%08X\n", symptr->st_size);
fprintf(stdout, "Symbol_type_and_binding: 0x%02X\n", symptr->st_info);
fprintf(stdout, "Section_index: 0x%04X\n", symptr->st_shndx);
fprintf(stdout,
"--------------------------------------------------------\n");
}

fprintf(stdout,
"************************************************************************\n");

next:

/* 读取 .dynstr 节的内容,并缓存在一个缓冲区中。*/
if ( lseek(fd, (off_t)SymNamStrTblFileOffset, SEEK_SET) !=
SymNamStrTblFileOffset || SymNamStrTblFileOffset == 0 ) {
fprintf(stderr, "lseek to Dynamical symbol name string error.\n");
close(fd); exit(1);
}
read(fd, SymNamStrTable, (size_t)(SymNamStrTblSize + 1));
if ( lseek(fd, (off_t)SymTblFileOffset, SEEK_SET) != SymTblFileOffset ||
SymTblFileOffset == 0 ) {
fprintf(stderr, "lseek to Dynamical symbol Table error.\n");
close(fd); exit(1);
}
for (i = 0; i %s\n",
symptr->st_name, SymNamStrTable + symptr->st_name);
fprintf(stdout, "Symbol_value: 0x%08X\n", symptr->st_value);
fprintf(stdout, "Symbol_size: 0x%08X\n", symptr->st_size);
fprintf(stdout, "Symbol_type_and_binding: 0x%02X\n", symptr->st_info);
fprintf(stdout, "Section_index: 0x%04X\n", symptr->st_shndx);
fprintf(stdout,
"--------------------------------------------------------\n");
}

fprintf(stdout,
"************************************************************************\n");

if ( lseek(fd, (off_t)HashOffset, SEEK_SET) != HashOffset ||
HashOffset == 0 ) {
fprintf(stderr, "lseek to hash table error.\n");
close(fd); exit(-1);
}
for (i = 0; i si_boundto);
fprintf(stdout, "Per_symbol_flags: 0x%04X\n", HashSymPtr->si_flags);
fprintf(stdout,
"--------------------------------------------------------\n");
}

close(fd); /* End of Printing */

/* Change symbol value, hook it */

if ( (fd = open(argv[1], O_RDWR)) st_name) == 0 ) {
memcpy(dst_sym, SymTblEntry, 16);
dst_sym_tbl = (unsigned int)lseek(fd, (off_t)0, SEEK_CUR) - 16;
}
if ( strcmp(argv[3], SymNamStrTable + symptr->st_name) == 0 ) {
memcpy(src_sym, SymTblEntry, 16);
src_sym_tbl = (unsigned int)lseek(fd, (off_t)0, SEEK_CUR) - 16;
}
}

/* 修改符号表中要修改的符号所对应的表项的 st_value 和 st_size 值。*/
symptr = (myElf32_Sym *)src_sym;
memcpy(tmp_sym_addr, &symptr->st_value, 4);
memcpy(tmp_sym_size, &symptr->st_size, 4);
symptr = (myElf32_Sym *)dst_sym;
memcpy(&symptr->st_value, tmp_sym_addr, 4);
memcpy(&symptr->st_size, tmp_sym_size, 4);

if ( dst_sym_tbl == 0 || src_sym_tbl == 0 ||
lseek(fd, (off_t)dst_sym_tbl, SEEK_SET) != dst_sym_tbl ) {
fprintf(stderr, "lseek error.\n");
close(fd); exit(-1);
}
if ( write(fd, dst_sym, (size_t)16) != (ssize_t)16 ) {
fprintf(stderr, "write error\n");
close(fd); exit(-1);
}

close(fd);
return 0;
}

/* EOF */


Before main() 分析

创建时间:2001-09-25
文章属性:原创
文章来源:http://www.xfocus.org/
文章提交:alert7 (sztcww_at_sina.com)

Before main() 分析


作者:alert7 <mailto: alert7@21cn.com
alert7@xfocus.org
>

主页: http://www.xfocus.org
时间: 2001-9-25


★ 前言

本文分析了在main()之前的ELF程序流程,试图让您更清楚的把握程序的流程的脉络走向。
从而更深入的了解ELF。不正确之处,还请斧正。


★ 综述

ELF的可执行文件与共享库在结构上非常类似,它们具有一张程序段表,用来描述这些段如何映射到进程空间.
对于可执行文件来说,段的加载位置是固定的,程序段表中如实反映了段的加载地址.对于共享库来说,段的加
载位置是浮动的,位置无关的,程序段表反映的是以0作为基准地址的相对加载地址.尽管共享库的连接是不
充分的,为了便于测试动态链接器,Linux允许直接加载共享库运行.如果应用程序具有动态链接器的描述段,
内核在完成程序段加载后,紧接着加载动态链接器,并且启动动态链接器的入口.如果没有动态链接器的描述段,
就直接交给用户程序入口。
上述这部分请参考:linuxforum论坛上opera写的《分析ELF的加载过程》

在控制权交给动态链接器的入口后,首先调用_dl_start函数获得真实的程序入口(注:该入口地址
不是main的地址,也就是说一般程序的入口不是main),然后循环调用每个共享object的初始化函数,
接着跳转到真实的程序入口,一般为_start(程序中的_start)的一个例程,该例程压入一些参数到堆栈,
就直接调用__libc_start_main函数。在__libc_start_main函数中替动态连接器和自己程序安排
destructor,并运行程序的初始化函数。然后才把控制权交给main()函数。



★ main()之前流程

下面就是动态链接器的入口。
/* Initial entry point code for the dynamic linker.
The C function `_dl_start' is the real entry point;
its return value is the user program's entry point. */

#define RTLD_START asm (" .text\n .globl _start\n .globl _dl_start_user\n _start:\n pushl %esp\n call _dl_start\n\/*该函数返回时候,%eax中存放着user entry point address*/
popl %ebx\n\/*%ebx放着是esp的内容*/
_dl_start_user:\n # Save the user entry point address in %edi.\n movl %eax, %edi\n\/*入口地址放在%edi*/

# Point %ebx at the GOT.
call 0f\n 0: popl %ebx\n addl $_GLOBAL_OFFSET_TABLE_+[.-0b], %ebx\n
# Store the highest stack address\n movl __libc_stack_end@GOT(%ebx), %eax\n movl %esp, (%eax)\n\/*把栈顶%esp放到GOT的__libc_stack_end中*/

# See if we were run as a command with the executable file\n # name as an extra leading argument.\n movl _dl_skip_args@GOT(%ebx), %eax\n movl (%eax), %eax\n
# Pop the original argument count.\n popl %ecx\n
# Subtract _dl_skip_args from it.\n subl %eax, %ecx\n
# Adjust the stack pointer to skip _dl_skip_args words.\n leal (%esp,%eax,4), %esp\n
# Push back the modified argument count.\n pushl %ecx\n
# Push the searchlist of the main object as argument in\n # _dl_init_next call below.\n movl _dl_main_searchlist@GOT(%ebx), %eax\n movl (%eax), %esi\n 0: movl %esi,%eax\n
# Call _dl_init_next to return the address of an initializer\n # function to run.\n call _dl_init_next@PLT\n\/*该函数返回初始化函数的地址,返回地址放在%eax中*/

# Check for zero return, when out of initializers.\n testl %eax, %eax\n jz 1f\n
# Call the shared object initializer function.\n # NOTE: We depend only on the registers (%ebx, %esi and %edi)\n # and the return address pushed by this call;\n # the initializer is called with the stack just\n # as it appears on entry, and it is free to move\n # the stack around, as long as it winds up jumping to\n # the return address on the top of the stack.\n call *%eax\n\/*调用共享object初始化函数*/

# Loop to call _dl_init_next for the next initializer.\n jmp 0b\n
1: # Clear the startup flag.\n movl _dl_starting_up@GOT(%ebx), %eax\n movl $0, (%eax)\n
# Pass our finalizer function to the user in %edx, as per ELF ABI.\n movl _dl_fini@GOT(%ebx), %edx\n
# Jump to the user's entry point.\n jmp *%edi\n .previous\n ");


sysdeps\i386\start.s中
user's entry也就是下面的_start例程

/* This is the canonical entry point, usually the first thing in the text
segment. The SVR4/i386 ABI (pages 3-31, 3-32) says that when the entry
point runs, most registers' values are unspecified, except for:

%edx Contains a function pointer to be registered with `atexit'.
This is how the dynamic linker arranges to have DT_FINI
functions called for shared libraries that have been loaded
before this code runs.

%esp The stack contains the arguments and environment:
0(%esp) argc
4(%esp) argv[0]
...
(4*argc)(%esp) NULL
(4*(argc+1))(%esp) envp[0]
...
NULL
*/

.text
.globl _start
_start:
/* Clear the frame pointer. The ABI suggests this be done, to mark
the outermost frame obviously. */
xorl %ebp, %ebp

/* Extract the arguments as encoded on the stack and set up
the arguments for `main': argc, argv. envp will be determined
later in __libc_start_main. */
popl %esi /* Pop the argument count. */
movl %esp, %ecx /* argv starts just at the current stack top.*/

/* Before pushing the arguments align the stack to a double word
boundary to avoid penalties from misaligned accesses. Thanks
to Edward Seidl for pointing this out. */
andl $0xfffffff8, %esp
pushl %eax /* Push garbage because we allocate
28 more bytes. */

/* Provide the highest stack address to the user code (for stacks
which grow downwards). */
pushl %esp

pushl %edx /* Push address of the shared library
termination function. */

/* Push address of our own entry points to .fini and .init. */
pushl $_fini
pushl $_init

pushl %ecx /* Push second argument: argv. */
pushl %esi /* Push first argument: argc. */

pushl $main

/* Call the user's main function, and exit with its value.
But let the libc call main. */
call __libc_start_main

hlt /* Crash if somehow `exit' does return. */



__libc_start_main在sysdeps\generic\libc_start.c中
假设定义的是PIC的代码。
struct startup_info
{
void *sda_base;
int (*main) (int, char **, char **, void *);
int (*init) (int, char **, char **, void *);
void (*fini) (void);
};

int
__libc_start_main (int argc, char **argv, char **envp,
void *auxvec, voi
d (*rtld_fini) (void),
struct startup_info *stinfo,
char **stack_on_entry)
{

/* the PPC SVR4 ABI says that the top thing on the stack will
be a NULL pointer, so if not we assume that we're being called
as a statically-linked program by Linux... */
if (*stack_on_entry != NULL)
{
/* ...in which case, we have argc as the top thing on the
stack, followed by argv (NULL-terminated), envp (likewise),
and the auxilary vector. */
argc = *(int *) stack_on_entry;
argv = stack_on_entry + 1;
envp = argv + argc + 1;
auxvec = envp;
while (*(char **) auxvec != NULL)
++auxvec;
++auxvec;
rtld_fini = NULL;
}

/* Store something that has some relationship to the end of the
stack, for backtraces. This variable should be thread-specific. */
__libc_stack_end = stack_on_entry + 4;

/* Set the global _environ variable correctly. */
__environ = envp;

/* Register the destructor of the dynamic linker if there is any. */
if (rtld_fini != NULL)
atexit (rtld_fini);/*替动态连接器安排destructor*/

/* Call the initializer of the libc. */

__libc_init_first (argc, argv, envp);/*一个空函数*/

/* Register the destructor of the program, if any. */
if (stinfo->fini)
atexit (stinfo->fini);/*安排程序自己的destructor*/

/* Call the initializer of the program, if any. */

/*运行程序的初始化函数*/
if (stinfo->init)
stinfo->init (argc, argv, __environ, auxvec);

/*运行程序main函数,到此,控制权才交给我们一般所说的程序入口*/
exit (stinfo->main (argc, argv, __environ, auxvec));

}



void
__libc_init_first (int argc __attribute__ ((unused)), ...)
{
}

int
atexit (void (*func) (void))
{
struct exit_function *new = __new_exitfn ();

if (new == NULL)
return -1;

new->flavor = ef_at;
new->func.at = func;
return 0;
}


/* Run initializers for MAP and its dependencies, in inverse dependency
order (that is, leaf nodes first). */

ElfW(Addr)
internal_function
_dl_init_next (struct r_scope_elem *searchlist)
{
unsigned int i;

/* The search list for symbol lookup is a flat list in top-down
dependency order, so processing that list from back to front gets us
breadth-first leaf-to-root order. */

i = searchlist->r_nlist;
while (i-- > 0)
{
struct link_map *l = searchlist->r_list[i];

if (l->l_init_called)
/* This object is all done. */
continue;

if (l->l_init_running)
{
/* This object's initializer was just running.
Now mark it as having run, so this object
will be skipped in the future. */
l->l_init_running = 0;
l->l_init_called = 1;
continue;
}

if (l->l_info[DT_INIT]
&& (l->l_name[0] != '' || l->l_type != lt_executable))
{
/* Run this object's initializer. */
l->l_init_running = 1;

/* Print a debug message if wanted. */
if (_dl_debug_impcalls)
_dl_debug_message (1, "\ncalling init: ",
l->l_name[0] ? l->l_name : _dl_argv[0],
"\n\n", NULL);

/*共享库的基地址+init在基地址中的偏移量*/
return l->l_addr + l->l_info[DT_INIT]->d_un.d_ptr;

}

/* No initializer for this object.
Mark it so we will skip it in the future. */
l->l_init_called = 1;
}


/* Notify the debugger all new objects are now ready to go. */
_r_debug.r_state = RT_CONSISTENT;
_dl_debug_state ();

return 0;
}
在main()之前的程序流程看试有点简单,但正在运行的时候还是比较复杂的
(自己用GBD跟踪下就知道了),因为一般的程序都需要涉及到PLT,GOT标号的
重定位。弄清楚这个对ELF由为重要,以后有机会再补上一篇吧。


★ 手动确定程序和动态连接器的入口

[alert7@redhat62 alert7]$ cat helo.c
#include
int main(int argc,char **argv)
{
printf("hello\n");
return 0;
}

[alert7@redhat62 alert7]$ gcc -o helo helo.c
[alert7@redhat62 alert7]$ readelf -h helo
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x8048320
Start of program headers: 52 (bytes into file)
Start of section headers: 8848 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 6
Size of section headers: 40 (bytes)
Number of section headers: 29
Section header string table index: 26
在这里我们看到程序的入口为0x8048320,可以看看是否为main函数。

[alert7@redhat62 alert7]$ gdb -q helo
(gdb) disass 0x8048320
Dump of assembler code for function _start:
0x8048320 : xor %ebp,%ebp
0x8048322 : pop %esi
0x8048323 : mov %esp,%ecx
0x8048325 : and $0xfffffff8,%esp
0x8048328 : push %eax
0x8048329 : push %esp
0x804832a : push %edx
0x804832b : push $0x804841c
0x8048330 : push $0x8048298
0x8048335 : push %ecx
0x8048336 : push %esi
0x8048337 : push $0x80483d0
0x804833c : call 0x80482f8
0x8048341 : hlt
0x8048342 : nop
End of assembler dump.
呵呵,不是main吧,程序的入口是个_start例程。

再来看动态连接器的入口是多少
[alert7@redhat62 alert7]$ ldd helo
libc.so.6 => /lib/libc.so.6 (0x40018000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
动态连接器ld-linux.so.2加载到进程地址空间0x40000000。

[alert7@redhat62 alert7]$ readelf -h /lib/ld-linux.so.2
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x1990
Start of program headers: 52 (bytes into file)
Start of section headers: 328916 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 3
Size of section headers: 40 (bytes)
Number of section headers: 23
Section header string table index: 20
共享object入口地址为0x1990。加上整个ld-linux.so.2被加载到进程地址空间0x40000000。
那么动态连接器的入口地址为0x1990+0x40000000=0x40001990。

用户空间执行的第一条指令地址就是0x40001990,既上面#define RTLD_START的开始。