2006年10月11日

云南印象

作者: Badcoffee
Email: blog.oliver@gmail.com
Blog: http://blog.csdn.net/yayong
2006年10月

    昆明.大理.丽江.西双版纳
    对出生在北方的我,是一个有着奇特风景和独特文化的地方…



2006年10月09日

两种消费观念

作者: Badcoffee
Email:
blog.oliver@gmail.com
Blog: http://blog.csdn.net/yayong
2006年10月

大学同班同学在北京的很多,大约十几个人。十一期间大学同学聚会,许多人都是久未谋面。

北京真的是太大了,以前租房的时候,大家都住在城里,来往频繁些。渐渐地,大家都买房成家,于是大都住在郊区,更甚者,五环之外占大多数。我住京西,你住京东,他住京北…于是见一次面不亚于城际旅行。记得以前从自己家所在城市到上大学所在的城市也不过就2小时吧。

聚会中,有同学开玩笑说,你看看,你们宿舍里的同学好几个都买了第二套房子,而他们宿舍买房之后都买了车。想想真是这样,我们宿舍竟然没有一位同学有车的。即便是被戏称为通州地产大鳄的老四,已经有3套房子了,也没有买车。

有车真的很好,从四惠东到三元桥去唱卡拉OK,不过一会儿的功夫,活动范围变大了,生活也变多彩了。
而有第二套房子,则再次沦为房奴,未来几年内,可能面临的还是还房贷的问题。

北京的房价还在变态的飞涨,也许我们这些房奴们在未来几年内资产增值而雀跃。或许,买房真的比买车子划算,以经济学的眼光来说,毕竟买车只是一种消费,而买房是一种投资?

不过,有时候还是禁不住问自己,放弃有车、有房、无贷款的安逸生活去辛苦供房,这样值得吗?

毕竟,生活不可以重来,失去的时光就永远失去了。 

2006年09月09日

换个角度思考问题

作者: Badcoffee
Email:
blog.oliver@gmail.com
Blog: http://blog.csdn.net/yayong
2006年9月

无意中在BBS上看到一篇帖子,作者是IBM lab的contractor,抱怨Regular Employee的能力不如自己,却很舒服得拿着比自己高的薪水云云。

看着帖子,突然有种亲切感从心底涌了出来,于是就有了这篇blog。

曾几何时,自己不是也有过这种怨气吗?

其实生活就是这样。从走出校门的那天起,第一个职位开始,就会遇到同样的情景。

你也许会发现,你很快就能够做和那些比你拿钱多的年长同事一样的事情,但是却拿着不同的报酬;

你又或发现,你的见识和能力比他强,但他却是你的领导;

于是自然有了一个结论:这个世界是不公平的。

然后,心态失衡,怨气就产生了。

我的职业生涯就是从这种怨气中开始了。

喜悦,满怀希望 -> 冷却,了解现实 -> 心态失衡,抱怨 -> 跳槽或升职 -> 喜悦,满怀希望

周而复始,完成一个个循环,自己也逐渐成长起来……

也许是岁数大了的缘故,渐渐地心态平和起来,那种怨气也渐渐消散了,虽然,依旧可能会一点点地心态失衡,然后跳槽或升职。

看来世界上没有绝对的公平,逐渐发现,这种不公平也变得合理起来。

尽管现在的我还是发现自己是很棒的,有些同事可能薪水比我高,但能力不如我。

生活每天都在继续,快乐是一天,不快乐也是一天,为什么不快乐的活着呢?

换个角度思考问题,也许会有不同的感受。

首先,理性分析;

不公平真的存在吗?不要忘了我们下意识的坏习惯;我们总是习惯于拿自己的优点去对照人家缺点。所以尽量试图发现他的优点、长处和自己的不足。也许你会有不同的结论。

其次,承认现实;

要承认时间,经验,人际关系,工作经历和教育背景在职业生涯中的重要作用。有时候,也许仅仅就是时间的积累。比如,一个在公司工作10多年的同事,虽然可能技术不如你,但是毕竟是10年的积累,不能够忽略,人际关系、经验和背景的因素。

最后,改变自己;

1. 想办法升职

找老板谈话,直接说出你的想法。询问自己不能得到提升的原因,和老板共同制定一个可以实施的计划。然后,按照计划努力达到要求。

2. 寻找跳槽机会

如果已经有很好的机会在等你了,干脆去和老板谈一次,然后作出果断选择。

表面上看,跳槽可能会快一些。公司内升职有时候会比较辛苦,需要你提前作出加倍的付出,还要承担老板承诺不能兑现的风险。

当你的职业发展还没有到达瓶颈的时候,这两种方法,都不成问题。但实际上,随着经验的积累,迟早我们也会面临职业发展的瓶颈。

所以,换另一个角度看问题,能发现薪水高但是水平不如你的人,是件好事。至少今天他的成就,很大可能明天你也能凭自己的能力达到。

真正该担忧的是,往前看,人家薪水高比你强,往后看,人家薪水低,比你也不弱。这时你的职业生涯恐怕就出现真正的瓶颈了。

突破瓶颈就是质的飞跃,而随波逐流,就沦为更平庸。生活就是这样,换个角度思考问题,也许会有不同的感受。

有人回帖说,因为地球是圆的,所以世界是不平的……觉得不错,作为结尾吧。

2006年04月16日

Kevin, an engineering manager, was frustrated. Good projects do not come to his engineers. If there are no projects, there will be no accomplishments, and no advancements. China will feel like the high-tech colony of the 21st century and get only menial works and be stuck in the "low wage" end of the industry.

I felt for Kevin’s despair. At the same time, I was astronished on how his young engineers have missed the basics and focused on the wrong things.

Like atheletes, good software engineers move up the career ladder will skills. It is quite simple, choose the arena, suit up and compete. The winners move on. The question is, "Are you training yourself everyday?"

The general categories for a software engineer evaluation are:

  • Credential:

    The pieces of paper that prove you qualified for certain tasks. The most basic ones are the diplomas. Then we have those issued by various institutes. These paper give the boss various degree of confidence that you can accomplish the tasks she needs you for. If the task has a commensurate higher job level, you may get the promotion tegether with the assignment.

    Credentials, particularly diplomas, also show your tenacity and conformity. You may not have learned much, but you stuck it out for years and met all requirements. If you can go through that, you must have what it will take to finish this project.

  • Experience:

    Young engineers hate to compete on experience. You are not qualified because you have never done this before. But how can you get the 1st project then?

  • Skills:

    Skills are easy. Although hard to describe precisely, an engineer know how skilled she is. Software, after all, is a craft. Within few minutes, an engineer would have sized up others and a pecking order is formed.

These basic three are part of all promotion considerations. The weights of them differ as career progresses. In general, Sun values credential the least, skills most, and experience a very close second. Ask. "Am I the most skilled and experienced among my peers?" Like atheletic competitions, software career is meritocratic. Do not waste efforts on anything before mastering the basics.

Next time, when you have finished the project, try do a bit extra:

  1. Testability:

    How is the code testable? Is there test programs that automate the process? When problems surface in the system, how easy it is to isolate the bug?

    Along the same line is demo. How would you show someone your proud achievement? Did you write another piece of program to show it off?

  2. Usability:

    Is there a web-based interface? Is there a tool to make it easier? How is the user experience? Do not hide behind areas of technology. A kernel module or a device driver can have user interfaces just like a Java application. As long as a human being can use it, you need to consider usability.

  3. Elegance:

    Is the code efficient and pretty? Nicely modulized, neatly formated, appropriately named, cleanly inferfaced? How did you treat the global state variables? How much you rely on side-effect to accomplish works? Are you proud of this piece of work like a poet?

  4. Documentation:

    Did you comment the code well? Did you write the design document? Did you let the next person who is unfortunately stuck with your code know where to pay attention?

These are where you gain experience without the assigned project. There is always something extra you can do to make your project better. When you are doing that extra amount of work, you gain experience on areas you do not normally practice in. Choose what you need to practice and do the extra work there.

And you would have done something wonderful to yourself. When you are doing those, your manager will notice that you walked the extra mile and did the extra-currriculum work. That shows your diligence, willingness to learn, and potential.

Most importantly, you gain control of your own career by doing these. Those extra works make you a better software engineer. An accomplished software engineer write his/her own ticket. You will name the next project you want to work on. Opportunities will come knocking on your door.

Lastly, wherever you look, there are bugs to be fixed and source code to be read. Pick an area of interest, read the source code, try to fix few bugs that seems trivial, talk to the owner of the code and ask him/her to review or integrate what you have done. They will appreciate it and you get the experience. This is the power of Sun’s openness. Don’t squander. Train yourself.

2006年04月11日

Web2.0和OpenSolaris



作者: Badcoffee


Email: blog.oliver@gmail.com

Blog: http://blog.csdn.net/yayong

2006年4月





OpenSolaris诞生之日起,当Solaris的Kernel Hacker们纷纷通过Blog发表揭示Solaris Kenrel如何工作的精彩文章时,OpenSolaris就似乎注定要和Web2.0结下不解之缘。



即便是现在,如果你试图Google一个Solaris Kernel的相关问题,那么很有可能你会发现某个Hacker写的Blog正静静躺在互联网一角。当然,maillist仍旧是Kernel Hacker们的最爱,正如LKML
无数Linux Kernel
newbie的每日必读一样。但是,无论是大部头系统介绍Kernel的书籍,还是maillist中邮件的问与答,都不能替代Blog的作用。书籍往往
是滞后与技术发展的,在Kernel研发领域更是如此,而mailist的传播能力则无法与Blog匹敌;而这一点,恰恰是Blog所擅长的。



Wiki则是将版本管理+内容发布的组合发挥到了极致。还有什么比Wiki更适合网络协作和知识管理的呢?事实上,在我们工作中,很多研发项目的项目主页就是用wiki来充当的,更不必说,包罗万象的维基百科全书了。



很多Linux Kernel newbie是从LKD,ULK,加上LKML一路走来。那么对OpenSolaris的Newbie,除了Solaris internal和OpenSolaris.org的maillist之外,你还有Kernal hacker的Blog,和OpenSolaris中文wiki




2006年03月14日

运“芯”帷幄 —CMP的操作系统技术     
2005-05-19

■ 清华大学计算机科学与技术系 董渊 林昊翔
■ 清华大学信息技术研究院 汪东升 李鹏


   单芯片多处理器(CMP),特别是在一个芯片上集成了多个相同通用处理器的单芯片对称多处理器(同构CMP)的发展,是上世纪90年代以来集成电路制造工艺的进步与微处理器体系结构的发展所带来的必然发展方向。

   和目前在服务器领域广泛采用的对称多处理器(Symmetric Multi-Processor, SMP)结构类似,在CMP系统中,位于同一个芯片内部所有处理器内核以平等的身份参与任务调度和中断处理,共享内存和外部设备,而且也可以共享片内的 (部分或全部)高速缓存。

   CMP的结构相对简单,可以直接使用现有的处理器内核,因此开发周期与成本相对较低,结构简单带来的另一个好处是更易获得高的主频。由于多个处理器集成在 一块芯片上,且共享cache,微处理器之间的通信延迟会明显降低,有利于提高系统的整体性能。因此,CMP具有良好的发展前景和广泛的应用空间,众多著 名大学、科研机构和商业公司都展开了广泛而积极的研究。

   而要想真正发挥CMP的优势,软件,特别是操作系统和编译工具等系统软件的支持至关重要,没有这些软件,CMP将处于“空转”状态。因此,每一个CMP系统都需要为其量身打造的系统软件。

   CMP对操作系统提出的挑战

   系统软件对于CMP广泛、深入的应用有重要的意义,这里我们讨论操作系统。操作系统是计算机系统的基本系统软件,在整个计算机系统中处于核心地位,负责控 制、管理计算机的所有软件、硬件资源,是惟一直接和硬件系统打交道的软件,是整个软件系统的基础部分,同时还为计算机用户提供良好的界面。

   对于普通用户而言,操作系统是一个资源管理者,通过它提供的系统命令和界面操作等工具,以某种易于理解的方式完成系统管理功能,有效地控制各种硬件资源,组织自己的数据,完成自己的工作并和其他人共享资源。

   对于程序员来讲,操作系统提供了一个与计算机硬件等价的扩展或虚拟的计算平台。操作系统提供给程序员的工具除了系统命令、界面操作之外,还有系统调用,系 统调用抽象了许多硬件细节,程序可以以某种统一的方式进行数据处理,程序员可以避开许多具体的硬件细节,提高程序开发效率,改善程序移植特性。

   并行是计算机科学与技术的重要分支之一,其核心思想是通过任务的合理划分和分配,使得多个处理器可以同时执行一个或多个任务,以达到系统整体计算能力的大 幅度提升。CMP的意义在于能够提供任务并行执行的一个新思路,支持在一个芯片内的多个处理器内核之间任务的划分和分配(也就是调度),而任务的调度则需 要操作系统来完成。

   CMP的发展对操作系统提出了新的挑战。首先,如何合理组织、调度任务才能最大程度地发挥CMP结构的性能?其次,如何保持操作系统的外部接口的相对稳 定?对于一般用户而言,大家希望的是平滑的过渡,一方面界面最好和以前的操作系统完全相同,另一方面以前能用的应用程序最好还能够不做任何修改就直接在 CMP的机器上直接运行,也就是说CMP对于用户来讲最好是透明的,这需要操作系统在用户界面和编程接口方面都保持不变。

   如何更好地组织和调度任务以便将CMP结构的性能发挥到极致是核心的问题,这是人们对CMP最大的期望。要解决这个问题,需要软硬件共同协作,从任务调 度、中断分配、资源共享等几个方面入手,硬件方面则要求CMP系统提供全新的同步与互斥、中断分配以及CPU内核之间的中断等机制。

   支持CMP操作系统的关键技术

   目前国际上对于CMP的研究还处于探索阶段,相关操作系统也处在积极研究时期。研究、分析和借鉴支持CMP操作系统的关键技术对于我们认识、理解和设计用 于CMP的操作系统有着非常重要的意义,这里,我们简单介绍支持CMP操作系统的引导和初始化、调度,中断处理和同步、互斥技术。

   系统引导和初始化

   操作系统的引导和初始化是指从系统加电到能够在多个处理器内核之间平等地进行任务调度的过程,这一过程是建立平等调度实施的基础,对于整个系统的运行具有 重要意义。虽然说对称多处理器系统中,各处理器可以平等地并行工作,但这是建立在系统有多个可并行执行任务的基础之上,而在引导和初始化过程中,由于很多 工作只能串行地执行,所以在这个阶段处理器内核是不平等的,是有主次之分的。系统加电之后,受到硬件控制,只启动其中一个处理器,称为主CPU或者引导处 理器(Booting Processor, BP),而其他处理器,称为次CPU或者应用处理器(Application Processor, AP),则处于停机等待状态。

   加电启动之后,主CPU跳转到特定的内存地址,通常映射到只读存储器,这里保存着整个计算机的引导程序bootloader,其任务是进行简单的硬件检 测、初始化环境参数、将操作系统内核装载到内存中,跳转到操作系统的起始地址并开始执行。这段引导过程完全由BP完成。

   进入操作系统内核之后,BP需要进行最初的草创性工作,完成运行环境准备、各种初识状态设置、基本读写数据段清零、bootloader传递过来的各种环 境参数保存、内存栈的开辟以及栈指针、全局指针设置。前面这部分工作全部由底层汇编代码完成,之后,BP跳转到由高级语言编写的函数,开始第二个阶段 CPU本身的初始化。

   在CPU初始化过程中,BP首先开始自检,收集CPU相关的指令集、存储管理、高速缓存以及协处理器等基本信息。接着为次CPU准备运行环境,同时为AP 准备一个锁,之后唤醒AP,AP转入主CPU设置好的地址,开始锁测试而进入等待状态。唤醒AP之后,BP输出自身信息之后,继续进行内存等各种资源的初 始化。

   接下来的工作主要有BP进行开发板以及外部设备初始化,之后准备用于所有CPU的空闲进程,这是一个不参与调度的进程,当某个CPU没有需要执行的任务, 就转入这个进程。准备好空闲进程之后,由BP解除对AP的锁,各AP逐个启动,进行各种关于各自CPU的初始化,将自身的状态填写到适当的数据结构,最后 相继进入空闲状态。

   所有的AP都完成初始化并进入空闲状态后,由BP来完成整个系统最后阶段的初始化,并执行系统的第一个进程,之后真正步入对称多处理器环境,所有的CPU进入正常、平等的调度。

   调度

   调度系统对于操作系统乃至整个计算机系统的整体性能有非常重要的影响,嵌入式、桌面和高端服务器系统对于调度器的要求是很不一样的。在CMP结构中调度机 制的重点在于更好地满足多处理机并行性上,核心思想是通过降低CPU间调度竞争和选择下一个运行进程的开销,以及提高系统整体负载平衡的能力,从而大幅度 提高多处理机系统的执行效率。其特性如下:

   1.调度算法

   在传统单CPU结构的调度系统中,所有就绪进程(状态为TASK_RUNNING)被组织到同一个双向链表之中,称为全局任务队列,调度过程中将遍历此链 表中的所有进程,调用计算每一个进程的权值,从中选择权值最大的进程投入运行。由于调度器要遍历所有就绪进程,因此选择下一个运行进程的时间复杂度是O (n)(n为就绪进程的个数)。同时,因为就绪队列是全局性的,对单CPU系统来讲只可能有一个CPU访问这个队列,而在多处理器结构中,必须通过一个全 局的自旋锁保证同一时刻只有一个CPU进行访问,这样导致系统中其他CPU的等待。如果就绪进程个数比较多,那么就绪队列就会成为一个明显的瓶颈。

   在支持CMP的操作系统中,每个CPU维护一个自己的就绪进程队列,称为局部任务队列,这样大大降低了CPU间的竞争。就绪进程按时间片是否用完分为 active和expired两大类,active类包括那些时间片没用完、当前可被调度的就绪进程,expired类包括那些时间片已用完的就绪进程。 同时,每类中的进程按照其优先级的不同处于不同的优先级链表中。

   调度时,active队列中非空的最高优先级链表的第一项被作为候选进程,使得选择下一个运行进程的操作可以在固定时间内完成。同时内核建立了一个位映射 数组来对应每一个优先级链表,使用标志位的方式极大降低寻找非空的链表所需要的时间。当一个进程耗尽其时间片后,内核重新计算它的优先级,并把它放置在 expired队列的相应优先级链表中。进程优先级的计算过程还可以分散到进程的执行过程中进行。这种将集中计算过程分散的做法,保证了调度器运行的时间 上限,降低了不必要的开销;同时在内存中保留更加丰富的信息的做法也加速了候选进程的定位过程。当active队列中没有可调度进程时,内核简单地对调 active和 expired队列,将原来的expired队列作为新的active队列后即可进行新一轮调度。

   此调度算法选择下一个运行进程的时间复杂度是O(1),与就绪进程的个数无关,调度效率大大提高。

   2.系统负载平衡

   支持CMP操作系统内核的调度系统需要很好地解决进程与CPU之间的“亲和”问题。如果不考虑亲和,一个进程可能在CPU之间比较频繁地迁移,交互式进程 (或高优先级的进程)可能还会在CPU之间不断“跳跃”,这样,每一次迁移之后,都可能造成频繁的内存访问,导致整体性能下降。

   支持CMP操作系统内核的调度系统尽量使得每个进程一直在固定的CPU上执行,这样可以提高Cache的命中率;但是如果某个CPU的就绪队列过长,不断 的进程切换反而造成Cache命中率的下降,而且还造成其他CPU不能充分发挥效能。无论当前CPU是繁忙或空闲,时钟中断每隔一段时间都会启动一次以平 衡负载。当然,一旦当前CPU发现自己的就绪队列为空时,也会主动进行负载平衡。

   为了进行有效的负载平衡,操作系统内核根据系统结构的特点,引入调度域(struct sched_domain)的概念将全体CPU一层一层地划分成不同的区域,每个调度域中的CPU分成若干个CPU组,且满足任一CPU惟一存在于一个组 中。每个CPU属于一个基本的调度域(该域至少包括本CPU),但是CPU同时还属于一个或多个更大的调度域。CPU的多个调度域通过构成一个单向链表, 且必须满足:1.父调度域是子调度域的超集;2.每个CPU的最高层调度域必须包括系统中的全部处理器。例如在一个支持超线程的CMP系统中,每个逻辑 CPU的基本调度域包含所在传统物理CPU上的全部的逻辑CPU,基本域的每个CPU分组包含一个逻辑CPU;基本调度域的父调度域是这个系统的最高层调 度域,它包含系统中所有的逻辑CPU,该域的每个CPU分组包含一个物理CPU上的全部逻辑CPU。

   对于CMP系统,每个芯片上的多个核天然地构成一层调度域。一个单芯片CMP系统和一个普通SMP系统的基本调度域的差别仅在于CMP基本调度域的CPU 组包含的对象是一个“CPU核”,而SMP基本调度域的CPU组包含的是一个传统的物理CPU。对于像支持超线程和多核SMP这样复杂的CMP系统,必须 增加一个基于芯片层面的新调度域。

   进行负载平衡时,从当前CPU的基本调度域出发,遍历所有的调度域。如果某个域有一段时间没有进行过负载平衡,先寻找域中负载最大的CPU组(CPU组的 负载等于组中所有CPU的负载之和),然后寻找组中最繁忙的CPU(该CPU的负载超过本CPU至少25%),更新双方的负载记录,确定需要迁移的进程数 为源CPU负载与本CPU负载之差的一半(经过更新后的值),然后按照从 expired 队列到 active 队列、从低优先级进程到高优先级进程的顺序进行迁移,但实际上真正执行迁移的进程往往少于计划迁移的数目。

   中断处理

   传统的单处理器通常都采用一个外部中断控制器来解决外部设备到CPU的通信,对于多处理器系统,处理器之间也需要通过中断方式进行通信,对于CMP而言, 多个处理器之间的中断控制器(本地中断控制器,简称本地控制器)也需要和处理器一起封装到芯片内部。此外,还需要一个全局的中断控制器(简称全局控制器) 负责各个处理器内核之间的中断分配,也要设计在芯片内部。

   1.中断分配

   全局中断控制器担负着把来自外部设备的中断请求提交和分配给片内各CPU的任务。对于每一个中断向量,可以采用静态或动态两种不同模式之一。如果某个中断 向量是静态分配的,则控制器把这种中断请求提交给预设的一个或多个CPU;动态模式则可以发送给所有CPU,或者随机地发送给某个处理器。

   本地控制器处理本处理器内部产生的中断请求、来自外部中断控制器的中断请求以及其他处理器发送过来的中断请求。

   2.处理器间中断

   在CMP系统中,芯片内部一个处理器常常要有目标地向系统中的其他处理器发出中断请求,这种中断被称为处理器间中断(IPI, Inter Processor Interrupt)。IPI至少应该包含以下两种:

   “重新调度”中断。当前CPU可以发送该中断来指示目标CPU可能需要一次进程调度,至于目标CPU在处理完该中断以后是否进行进程调度,那得要看事先或者在处理中断的过程中是否把当前进程设置为需要调度。

   “请求执行”中断。这个中断被用来请求目标CPU执行一个指定的函数。之所以要通过IPI请其他CPU执行,是因为某个函数必须由目标CPU才能完成,而 不能由别的CPU代替。比如某个处理器改变了内存中某个页面映射目录或页面映射表的内容,从而可能引起其他处理器的TLB与其不一致时,就向系统中正在使 用这个映射表的处理器发送这个中断,请它们自己执行代码,废弃各自TLB的内容。

   3.时钟中断

   在所有中断请求中,时钟中断扮演着特殊的重要角色,每一个处理器都有自身的时钟发生器。在系统初始化阶段,系统先设置一个外部时钟中断源供所有CPU共 享,并且以此基准测算各个CPU的运算速度,并校准自身的时钟中断发生间隔。由于各处理器设计完全一致,所有CPU都有基本相同的时钟脉冲周期,为了不让 所有处理器都在同一时刻发生时钟中断,操作系统应该使各个CPU的时钟中断在相位上相互错开,把这些中断均匀地分布在时钟中断的周期中。

   同步与互斥技术

   在多任务系统中,不同的任务可能需要同时访问某些共享变量和共享资源,形成了竞争的关系。这就需要系统提供同步与互斥机制,使得这些共享变量和共享资源同 一时刻只能被一个任务以独占的方式访问。传统单处理器系统中在宏观上是并行,微观上,由于只有一个处理器,同一时刻只能执行一个任务,同步和互斥的问题相 对比较容易解决,而CMP的操作系统在运行时,同一时刻有可能有多个任务执行,传统的方法有时不能满足这种特定的情况,需要引入新的机制。

   同步和互斥机制需要底层硬件提供“读-修改-写”类型的访存原子操作。这样的操作能够让CPU从主存储器中读取一个值,修改之,再将修改过的值保存到存储 器的相同位置中。整个过程是一次完整的总线交易,不能被其他CPU内核的访存操作所打断。“读-修改-写”类型的原子操作有多种实现方式,最常见的包括 test_and_set,swap,load-linked/store-conditional等方式。

   Test_and_set指令从主存储器中读取一个值(通常是一个字节或一个字),将它和0比较并根据比较的结果设置条件码,最后将1无条件地存放到存储 器的相应单元。一旦一条test_and_set指令开始了它的总线周期,那么任何其他CPU都不能访问主存储器。

   在Intel的处理器中,CPU芯片有一个LOCK引脚,如果汇编语言的程序中在一条指令前加上前缀“LOCK”,经过汇编厚的机器代码就使CPU在执行 这条指令时将LOCK引脚拉低,将总线锁上,这时总线上的其他CPU暂时不能进行总线操作,从而保证了操作的原子性。X86系列的CPU还提供一条 xchg指令,提供了原子的交换操作,这条指令不论前面是否加LOCK前缀,都是一个原子操作。

   在MIPS等RISC处理器中,对原子操作进行了简化,他们提供了一对指令(load-linked/store-conditional),一起使用这 一对指令就能执行一次原子的“读-修改-写”操作。Load-linked指令执行原子“读-修改-写”操作的前半部分,它从存储器中读取一个值,在硬件 中设置一个标志,表示正在进行原子操作,并设置读取的地址。然后使用store-conditional指令完成修改操作。这样对于相应的存储器位置来 说,从load-linked开始到store-conditional结束的整个指令序列都是以原子的方式执行的。

   操作系统可以使用根据上述原理编写的自旋锁(spin-lock)。它的作用是取得一个变量(称之为锁)的访问权限,如果成功,则进行下一步操作,如对共 享变量或共享资源的访问,如果没有成功,则一直查询该锁,直到成功取得该锁的访问权限为止。自旋锁是保证同步和互斥操作的重要操作。

   利用硬件提供的“读-修改-写”原子,操作系统可以完成各种同步和互斥操作,正确地解决资源的共享问题。

2006年03月08日


Solaris学习笔记(3)

作者: Badcoffee
Email: blog.oliver@gmail.com
Blog: http://blog.csdn.net/yayong
2006年3月


很久以前就看过alert7写的那篇ELF 动态解析符号过程(修订版),大概是他在学习ELF文件格式时写的吧。OpenSolaris之后,其内核所有代码全世界都可以访问到,于是就有了这 篇文章。本文仅用于学习交流目的,因此没有经过严格校对,错误再所难免,如果有勘误或疑问请与我联系。

关键词:Dynamic binding/ld.so/mdb/link map/Solaris

1. 基本概念

Link-Editor – 链接器:即ld(1),输入一个或多个输入文件(*.o/*.so/*.a),经过连接和解释数据,输出一个目标文件(*.o/*.so/*.a/可执行 文件)。ld通常作为编译环境的一部分来执行。

Runtime Linker – 动态链接器: 即ld.so.1(1), 在运行时刻处理动态的可执行程序和共享库,把可执行程序和共享库绑定在一起创建一个可执行的进程。

Shared objects – 共享对象: 也叫共享库,是动态链接系统的基础。共享对象类似与动态可执行文件,但共享对象没有被指定虚拟内存地址。 共享对象可以在系统中多个应用程序共同使用和共享。

Dynamic executables – 动态可执行文件:通常依赖于一个或者多个共享对象。 为了产生一个可以执行的进程,一个或者多个共享对象必须绑定在动态可执行文件上。

runtime linker主要负责以下几方面工作:

1.分析可执行文件中包含的动态信息部分(对ELF文件来说就是.dynamic section)来决定该文件运行所需的依赖库;
2.定位和装载这些依赖库,分析这些依赖库所包含的动态信息部分,来决定是否需装载要任何附加的依赖库;
3.对动态库进行必要的重定位,在进程的执行期间绑定这些对象;
4.调用这些依赖库提供的初始化函数(ELF文件来说就是.init section,而且顺序是先执行依赖库的,再执行可执行文件的);
5.把控制权转交给应用程序;
6.在应用程序执行期间,能被再调用,来执行延后的函数绑定(即动态解析);
7.在应用程序调用dlopen(3C)打开动态库和用dlsym(3C)绑定这些库的符号时,也要被调用;


2. 测试与验证


写一个最简的测试程序test.c:

#include <stdio.h>
int main(int agrc, char *argv[])
{
printf ("hello world\n");
return 0;
}


编译和链接后产生ELF文件:

# cc test.c -o test
# file test
test: ELF 32-bit LSB executable 80386 Version 1, dynamically linked, not stripped


用mdb反汇编main函数:

# mdb test
> main::dis
main: pushl %ebp
main+1: movl %esp,%ebp
main+3: subl $0x10,%esp
main+6: movl %ebx,-0x8(%ebp)
main+9: movl %esi,-0xc(%ebp)
main+0xc: movl %edi,-0x10(%ebp)
main+0xf: pushl $0x80506ec
main+0x14: call -0x148 <PLT:printf>
main+0x19: addl $0x4,%esp
main+0x1c: movl $0x0,-0x4(%ebp)
main+0x23: jmp +0x5 <main+0x28>
main+0x28: movl -0x4(%ebp),%eax
main+0x2b: movl -0x8(%ebp),%ebx
main+0x2e: movl -0xc(%ebp),%esi
main+0x31: movl -0x10(%ebp),%edi
main+0x34: leave
main+0x35: ret


可以看到,main+0×14处调用了函数printf,调用前把传递的字符串参数压入栈:

> 0x80506ec/s
0x80506ec: hello world


“hello world”在ELF文件的.rodata1 section,处于test的代码段:

# /usr/ccs/bin/elfdump -c -N .rodata1 test

Section Header[13]: sh_name: .rodata1
sh_addr: 0x80506ec sh_flags: [ SHF_ALLOC ]
sh_size: 0xd sh_type: [ SHT_PROGBITS ]
sh_offset: 0x6ec sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x4


用mdb在main+0×14处设置断点,然后运行程序:

> main+0x14:b
> :r
mdb: stop at main+0x14
mdb: target stopped at:
main+0x14: call -0x148 <PLT:printf>

程序在调用printf之前停止,我们计算一下printf的地址:


> main+0x14-0x148=X
8050544


验证一下,地址0×8050544是否正确:


# /usr/ccs/bin/elfdump -s -N .symtab test | grep printf
[38] 0x08050544 0x00000000 FUNC GLOB D 0 UNDEF printf
# /usr/ccs/bin/elfdump -s -N .dynsym test | grep printf
[1] 0x08050544 0x00000000 FUNC GLOB D 0 UNDEF printf

在test文件的.symtab和.dynsym section都可以找到符号表中包含printf,符号表实际上是一个数组,数组元素定义如下:

typedef struct {
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_Sym;


printf的st_value就是0×08050544,在ELF的可执行文件中,这就是printf的虚存地址,而这恰好就是我们mdb中计算的地 址。

我们同样可以用nm(1)命令确认这一点:

# /usr/ccs/bin/nm -x test | grep printf
[Index] Value Size Type Bind Other Shndx Name
......
[38] |0x08050544|0x00000000|FUNC |GLOB |0 |UNDEF |printf

printf的st_shndx的值是UNDEF,说明printf未在test中定义。既然程序可以链接通过,那么printf肯定存在于它依赖的共享 库中。

test依赖的共享库如下:

# ldd test
libc.so.1 => /lib/libc.so.1
libm.so.2 => /lib/libm.so.2

当一个程序有多个共享库依赖时,runtime linker是按照一定的顺序运行各个库的.init函数的,即前面提到的步骤4,查看顺序用ldd -i:

# ldd -i /usr/bin/cp
libcmdutils.so.1 => /lib/libcmdutils.so.1
libavl.so.1 => /lib/libavl.so.1
libsec.so.1 => /lib/libsec.so.1
libc.so.1 => /lib/libc.so.1
libm.so.2 => /lib/libm.so.2

init object=/lib/libc.so.1
init object=/lib/libavl.so.1
init object=/lib/libcmdutils.so.1
init object=/lib/libsec.so.1

test依赖的库只有libc(3LIB)和libm(3LIB),libm是数学库,因此printf一定在libc(3LIB)中。我们知道,在 libc(3LIB)库中,包含了System V, ANSI C, POSIX等多种标准的函数实现。

查看libc.so的符号表中的printf:

# /usr/ccs/bin/nm -x /usr/lib/libc.so | grep  "|printf___FCKpd___13quot;
[Index] Value Size Type Bind Other Shndx Name
......
[7653] |0x00061f39|0x00000105|FUNC |GLOB |0 |11 |printf

libc.so中printf的st_value是0×00061f39,由于libc.so是一个共享库,因此这个地址只是printf在 libc.so中的偏移量,需要和libc.so的加载地址相加才可以得出真正的虚存地址,而这个地址才是真正的printf函数的代码入口。

libc.so中printf的st_shndx的值为11,当st_shndx是数值是,代表改函数所在的section header的索引号:

# /usr/ccs/bin/elfdump -c /usr/lib/libc.so | grep 11
Section Header[11]: sh_name: .text
sh_size: 0x110 sh_type: [ SHT_SUNW_SIGNATURE ]

ELF文件test中的.symtab和.dynsym都包含了printf,而且st_value都相同,但是我们看到如果strip以后,nm命令没 有输出,这是因为test文件中的.symtab section被去除的原因:

# /usr/ccs/bin/strip test
# /usr/ccs/bin/elfdump -s -N .symtab test | grep printf
# /usr/ccs/bin/nm -x test1 | grep printf
# /usr/ccs/bin/elfdump -s -N .dynsym test | grep printf
[1] 0x08050544 0x00000000 FUNC GLOB D 0 UNDEF printf


实际上只有.dynsym才被影射入内存,.dynsym是实现动态链接必须的信息,.symtab根本不会影射入内存。

在test创建的进程中,printf位于地址8050544,用mdb反汇编printf的代码:

> 8050544::dis
PLT:printf: jmp *0x8060714
PLT:printf: pushl $0x18
PLT:printf: jmp -0x4b <0x8050504>
PLT:_get_exit_frame_monitor: jmp *0x8060718
PLT:_get_exit_frame_monitor: pushl $0x20
PLT:_get_exit_frame_monitor: jmp -0x5b <0x8050504>
................


可以看到,实际上,printf的代码只有3条指令,显然,这并不是真正printf的实现,而是叫做PLT的其中部分代码。


Global Offset Table – 全局偏移量表:GOT存在于可执行文件的数据段中,用于存放位置无关函数的绝对地址。GOT表中的绝对地址实际上是在运行阶段时,在位置无关函数首次被 runtime linker解析后才确定。在此之前,GOT中的初值主要是为了帮助PLT跳转到runtime linker,把控制权转交给它的动态绑定函数。

其实,.got的初值在test文件中已经定义:

# /usr/ccs/bin/elfdump -c -N .got test

Section Header[14]: sh_name: .got
sh_addr: 0x80606fc sh_flags: [ SHF_WRITE SHF_ALLOC ]
sh_size: 0x20 sh_type: [ SHT_PROGBITS ]
sh_offset: 0x6fc sh_entsize: 0x4
sh_link: 0 sh_info: 0
sh_addralign: 0x4

# /usr/ccs/bin/elfdump -G test

Global Offset Table Section: .got (8 entries)
ndx addr value reloc addend symbol
[00000] 080606fc 0806071c R_386_NONE 00000000
[00001] 08060700 00000000 R_386_NONE 00000000
[00002] 08060704 00000000 R_386_NONE 00000000
[00003] 08060708 0805051a R_386_JMP_SLOT 00000000 atexit
[00004] 0806070c 0805052a R_386_JMP_SLOT 00000000 __fpstart
[00005] 08060710 0805053a R_386_JMP_SLOT 00000000 exit
[00006] 08060714 0805054a R_386_JMP_SLOT 00000000 printf
[00007] 08060718 0805055a R_386_JMP_SLOT 00000000 _get_exit_frame_monitor


可以看到,在ELF文件中的GOT共有8个表项:

    GOT[0]是保留项,被初始化为.dynamic section的起始地址。
    GOT[1]和GOT[2]初值为0,在装入内存后初始化。
    GOT[3]-GOT[7],被初始化成了对应符号的在PLT中第2条指令的地址。

GOT的结束地址也可以根据section header中的sh_size计算出来:


> 0x80606fc+20=X
806071c

而test运行到main+0×14断点处,查看GOT:

> 0x80606fc,9/naX
0x80606fc:
0x80606fc: 806071c
0x8060700: d17fd900
0x8060704: d17cb260
0x8060708: d1710814
0x806070c: d1701e51
0x8060710: 805053a
0x8060714: 805054a
0x8060718: 805055a
0x806071c: 1


可以看到,GOT的内容和ELF文件定义的初始值相比,有了一些变化:

> 0x80606fc,9/nap
0x80606fc:
0x80606fc: 0x806071c --->未改变,.dynamic section的起始地址
0x8060700: 0xd17fd900 --->改变,Rt_map首地址,也是link_map首地址
0x8060704: ld.so.1`elf_rtbndr --->改变,Runtime linker的入口
0x8060708: libc.so.1`atexit --->改变,已经被ld.so解析成绝对地址
0x806070c: libc.so.1`_fpstart --->改变,已经被ld.so解析成绝对地址
0x8060710: PLT:exit --->未改变,还未解析,指向PLT:exit的第2条指令
0x8060714: PLT:printf --->未改变,还未解析,指向PLT:printf的第2条指令
0x8060718: PLT:_get_exit_frame_monitor --->未改变,还未解析,指向PLT:_get_exit_frame_monitor的第2条指令
0x806071c: 1



在此时,runtim linker把link map和自己的入口函数地址填入了GOT[1]和GOT[2]中,并且atexit和_fpstart已经被解析成绝对地址。这是因为每个可执行文件的实际入口是_start例程,这个例程执行中会调用atexit和_fpstart,然后才调用main函数:

> _start::dis
_start: pushl $0x0
_start+2: pushl $0x0
_start+4: movl %esp,%ebp
_start+6: pushl %edx
_start+7: movl $0x806071c,%eax
_start+0xc: testl %eax,%eax
_start+0xe: je +0x7 <_start+0x15>
_start+0x10: call -0x64 <PLT:atexit>
_start+0x15: pushl $0x80506cc
_start+0x1a: call -0x6e <PLT:atexit>
_start+0x1f: leal 0x80607f4,%eax
_start+0x25: movl (%eax),%eax
_start+0x27: testl %eax,%eax
_start+0x29: je +0x17 <_start+0x40>
_start+0x2b: leal 0x80607f8,%eax
_start+0x31: movl (%eax),%eax
_start+0x33: testl %eax,%eax
_start+0x35: je +0xb <_start+0x40>
_start+0x37: pushl %eax
_start+0x38: call -0x8c <PLT:atexit>
_start+0x3d: addl $0x4,%esp
_start+0x40: movl 0x8(%ebp),%eax
_start+0x43: movl 0x80607d4,%edx
_start+0x49: testl %edx,%edx
_start+0x4b: jne +0xc <_start+0x57>
_start+0x4d: leal 0x10(%ebp,%eax,4),%edx
_start+0x51: movl %edx,0x80607d4
_start+0x57: andl $0xfffffff0,%esp
_start+0x5a: pushl %edx
_start+0x5b: leal 0xc(%ebp),%edx
_start+0x5e: movl %edx,0x80607f0
_start+0x64: pushl %edx
_start+0x65: pushl %eax
_start+0x66: call -0xaa <PLT:__fpstart>
_start+0x6b: call +0x29 <__fsr>
_start+0x70: call +0xd8 <_init>
_start+0x75: call +0x9b <main>
_start+0x7a: addl $0xc,%esp
_start+0x7d: pushl %eax
_start+0x7e: call -0xb2 <PLT:exit>
_start+0x83: pushl $0x0
_start+0x85: movl $0x1,%eax
_start+0x8a: lcall $0x7,$0x0
_start+0x91: hlt


Procedure Linkage Table – 过程链接表:PLT存在于每个ELF可执行文件的代码段,它和可执行文件的数据段中的GOT来一起决定位置无关函数的绝对地址。首先,第一次调用位置无关函数时,会进入相应函数的PLT入口,PLT的指令会从GOT中读出默认地址,该地址正好是PLT0的入口地址,PLT0会把控制权交给runtime linker,由runtime linker解析出该函数的绝对地址,然后将这个绝对地址存入GOT,然后,该函数将被调用。然后,当再次调用该函数时,由于GOT中已经存放了该函数入口的绝对地址,因此PLT对应的指令会直接跳转到函数绝对地址,而不会再由runtime linker解析。

PLT的一般格式如下:

.PLT0:pushl got_plus_4
      jmp *got_plus_8
      nop; nop
      nop; nop
.PLT1:jmp *name1_in_GOT
      pushl $offset@PC
      jmp .PLT0@PC …
.PLT2:jmp *name2_in_GOT
      push $offset
      jmp .PLT0@PC
.PLT2:jmp *name3_in_GOT
      push $offset
      jmp .PLT0@PC


可以通过elfdump来实际查看test文件验证一下:

# /usr/ccs/bin/elfdump -c -N .plt test

Section Header[8]: sh_name: .plt
sh_addr: 0x8050504 sh_flags: [ SHF_ALLOC SHF_EXECINSTR ]
sh_size: 0x60 sh_type: [ SHT_PROGBITS ]
sh_offset: 0x504 sh_entsize: 0x10
sh_link: 0 sh_info: 0
sh_addralign: 0x4


这样,PLT的结束地址也可以计算出来:

> 0x8050504+0x60=X
8050564

根据.plt的起始和结束地址可以反汇编:

> 0x8050504::dis -a -n 13
8050504 pushl 0x8060700 ---->pushl got_plus_4,指向Rt_map地址
805050a jmp *0x8060704 ---->jmp *got_plus_8,跳转到Runtime linker的入口
8050510 addb %al,(%eax)
8050512 addb %al,(%eax)
8050514 jmp *0x8060708
805051a pushl $0x0
805051f jmp -0x1b <0x8050504>
8050524 jmp *0x806070c
805052a pushl $0x8
805052f jmp -0x2b <0x8050504>
8050534 jmp *0x8060710
805053a pushl $0x10
805053f jmp -0x3b <0x8050504>
8050544 jmp *0x8060714 ---->跳转到0x805054a,即下一条指令
805054a pushl $0x18
805054f jmp -0x4b <0x8050504>
8050554 jmp *0x8060718
805055a pushl $0x20
805055f jmp -0x5b <0x8050504>
8050564 addb %al,(%eax)


或者包含符号信息:

> 0x8050504::dis -n 13
0x8050504: pushl 0x8060700
0x805050a: jmp *0x8060704
0x8050510: addb %al,(%eax)
0x8050512: addb %al,(%eax)
PLT=libc.so.1`atexit: jmp *0x8060708
PLT=libc.so.1`atexit: pushl $0x0
PLT=libc.so.1`atexit: jmp -0x1b <0x8050504>
PLT=libc.so.1`_fpstart: jmp *0x806070c
PLT=libc.so.1`_fpstart: pushl $0x8
PLT=libc.so.1`_fpstart: jmp -0x2b <0x8050504>
PLT:exit: jmp *0x8060710
PLT:exit: pushl $0x10
PLT:exit: jmp -0x3b <0x8050504>
PLT:printf: jmp *0x8060714
PLT:printf: pushl $0x18
PLT:printf: jmp -0x4b <0x8050504>
PLT:_get_exit_frame_monitor: jmp *0x8060718
PLT:_get_exit_frame_monitor: pushl $0x20
PLT:_get_exit_frame_monitor: jmp -0x5b <0x8050504>
0x8050564: addb %al,(%eax)



在main+0×14处,继续单步运行:

> :s
mdb: target stopped at:
PLT:printf: jmp *0x8060714

查看0×8060714即printf在GOT中的内容,其实就是PLT:printf中下一条push指令:


> *0x8060714=X
805054a
> *0x8060714::dis -n 1
PLT:printf: pushl $0x18
PLT:printf: jmp -0x4b <0x8050504>


继续单部执行,马上就要把0×18压入栈,这个0×18就是printf在重定位表中的偏移量:

# /usr/ccs/bin/elfdump -c -N .rel.plt test

Section Header[7]: sh_name: .rel.plt
sh_addr: 0x80504dc sh_flags: [ SHF_ALLOC SHF_INFO_LINK ]
sh_size: 0x28 sh_type: [ SHT_REL ]
sh_offset: 0x4dc sh_entsize: 0x8
sh_link: 3 sh_info: 8
sh_addralign: 0x4

# /usr/ccs/bin/elfdump -d test

Dynamic Section: .dynamic
index tag value
[0] NEEDED 0x111 libc.so.1
[1] INIT 0x80506b0
[2] FINI 0x80506cc
[3] HASH 0x80500e8
[4] STRTAB 0x805036c
[5] STRSZ 0x137
[6] SYMTAB 0x80501cc
[7] SYMENT 0x10
[8] CHECKSUM 0x5a2b
[9] VERNEED 0x80504a4
[10] VERNEEDNUM 0x1
[11] PLTRELSZ 0x28
[12] PLTREL 0x11
[13] JMPREL 0x80504dc ---> 重定位表.rel.plt的基地址
[14] REL 0x80504d4
[15] RELSZ 0x30
[16] RELENT 0x8
[17] DEBUG 0
[18] FEATURE_1 0x1 [ PARINIT ]
[19] FLAGS 0 0
[20] FLAGS_1 0 0
[21] PLTGOT 0x80606fc



直接查看重定位表内容:

# /usr/ccs/bin/elfdump -r  test

Relocation Section: .rel.data
type offset section with respect to
R_386_32 0x80607f8 .rel.data __1cG__CrunMdo_exit_code6F_v_

Relocation Section: .rel.plt
type offset section with respect to
R_386_JMP_SLOT 0x8060708 .rel.plt atexit
R_386_JMP_SLOT 0x806070c .rel.plt __fpstart
R_386_JMP_SLOT 0x8060710 .rel.plt exit
R_386_JMP_SLOT 0x8060714 .rel.plt printf
R_386_JMP_SLOT 0x8060718 .rel.plt _get_exit_frame_monitor


其中,printf是4项,而在32位x86平台上,重定位表的每项的长度为8字节,定义如下:

typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;

因此,printf在重定位表中偏移量=(4-1)*8=24,即16进制的0×18。

用mdb查看实际内存中的重定位表:

> 0x80504dc,a/nap
0x80504dc:
0x80504dc: 0x8060708
0x80504e0: 0xf07
0x80504e4: 0x806070c
0x80504e8: 0x1007
0x80504ec: 0x8060710
0x80504f0: 0x1207
0x80504f4: 0x8060714
0x80504f8: 0x107
0x80504fc: 0x8060718
0x8050500: 0x1307


可以看到,printf的r_offset是0×8060714,r_info是0×107。对照前面的GOT各项的地址,可以发现,0×8060714 就是GOT[7]的地址。


> :s
mdb: target stopped at:
PLT:printf: pushl $0x18

继续单步执行:

> :s
mdb: target stopped at:
PLT:printf: jmp -0x4b <0x8050504>

地址0×8050504就是PLT0的地址:


> :s
mdb: target stopped at:
0x8050504: pushl 0x8060700

0×8060700就是GOT[1],存储的就是Rt_map的首地址,相当于把Rt_map的首地址压栈:


> :s
mdb: target stopped at:
0x805050a: jmp *0x8060704

0×8060704就是GOT[2],存储着runtime linker – ld.so的入口地址:

> :s
mdb: target stopped at:
ld.so.1`elf_rtbndr: pushl %ebp

可以看到,这样控制权就由PLT这样转换到runtime linker了,显然,下面将进入runtime link editor来动态绑定了,我们查看目前栈的状态:

> <esp,10/nap
0x804734c:
0x804734c: 0xd17fd900 ----> Rt_map的首地址
0x8047350: 0x18 ----> printf对应项重定位表中的偏移量
0x8047354: main+0x19 ----> printf返回后应跳转的地址
0x8047358: 0x80506ec
0x804735c: 0x8047460
0x8047360: 0x8047354
0x8047364: 0xd17fb840
0x8047368: 0x8047460
0x804736c: 0x804738c
0x8047370: _start+0x7a
0x8047374: 1
0x8047378: 0x8047398
0x804737c: 0x80473a0
0x8047380: _start+0x1f
0x8047384: _fini
0x8047388: ld.so.1`atexit_fini


查看ld.so.1`elf_rtbndr函数的定义,这部分是平台相关的,我们只关心32bit x86部分的实现:

link:http://cvs.opensolaris.org/source/xref/on/usr/src/cmd/sgs/rtld/i386/boot_elf.s

   288 #if defined(lint)
289
290 extern unsigned long elf_bndr(Rt_map *, unsigned long, caddr_t);
291
292 void
293 elf_rtbndr(Rt_map * lmp, unsigned long reloc, caddr_t pc)
294 {
295 (void) elf_bndr(lmp, reloc, pc);
296 }
297
298 #else
299 .globl elf_bndr
300 .globl elf_rtbndr
301 .weak _elf_rtbndr
302 _elf_rtbndr = elf_rtbndr / Make dbx happy
303 .type elf_rtbndr,@function
304 .align 4
305
306 elf_rtbndr:
307 pushl %ebp
308 movl %esp, %ebp
309 pushl %eax
310 pushl %ecx
311 pushl %edx
312 pushl 12(%ebp) / push pc
313 pushl 8(%ebp) / push reloc
314 pushl 4(%ebp) / push *lmp
315 call elf_bndr@PLT / call the C binder code
316 addl $12, %esp / pop args
317 movl %eax, 8(%ebp) / store final destination
318 popl %edx
319 popl %ecx
320 popl %eax
321 movl %ebp, %esp
322 popl %ebp
323 addl $4,%esp / pop args
324 ret / invoke resolved function
325 .size elf_rtbndr, .-elf_rtbndr
326 #endif


315行调用的elf_bndr是平台相关代码,函数原型如下:

   290 extern unsigned long    elf_bndr(Rt_map *, unsigned long, caddr_t);

因此在elf_rtbndr的312-314这几行,实际上是为调用elf_bndr做传递参数的准备:

   312     pushl    12(%ebp)        / push返回地址 main+0x19
313 pushl 8(%ebp) / push重定位表的对应printf项的偏移量 0x18
314 pushl 4(%ebp) / push Rt_map的首地址,0xd17fd900

根据32位x86的ABI,压栈顺序是从右到左,正好吻合elf_bndr的参数顺序和类型定义。

通过在elf_bndr函数调用前设置断点来验证一下:

> ld.so.1`elf_rtbndr::dis
ld.so.1`elf_rtbndr: pushl %ebp
ld.so.1`elf_rtbndr+1: movl %esp,%ebp
ld.so.1`elf_rtbndr+3: pushl %eax
ld.so.1`elf_rtbndr+4: pushl %ecx
ld.so.1`elf_rtbndr+5: pushl %edx
ld.so.1`elf_rtbndr+6: pushl 0xc(%ebp)
ld.so.1`elf_rtbndr+9: pushl 0x8(%ebp)
ld.so.1`elf_rtbndr+0xc: pushl 0x4(%ebp)
ld.so.1`elf_rtbndr+0xf: call +0x14c5d <ld.so.1`elf_bndr>
ld.so.1`elf_rtbndr+0x14: addl $0xc,%esp
ld.so.1`elf_rtbndr+0x17: movl %eax,0x8(%ebp)
ld.so.1`elf_rtbndr+0x1a: popl %edx
ld.so.1`elf_rtbndr+0x1b: popl %ecx
ld.so.1`elf_rtbndr+0x1c: popl %eax
ld.so.1`elf_rtbndr+0x1d: movl %ebp,%esp
ld.so.1`elf_rtbndr+0x1f: popl %ebp
ld.so.1`elf_rtbndr+0x20: addl $0x4,%esp
ld.so.1`elf_rtbndr+0x23: ret
> ld.so.1`elf_rtbndr+0xf:b
> :c
mdb: stop at ld.so.1`elf_rtbndr+0xf
mdb: target stopped at:
ld.so.1`elf_rtbndr+0xf: call +0x14c5d <ld.so.1`elf_bndr>

下面检查ld.so.1`elf_bndr调用前栈的状况,可以看到,3个参数已经按顺序压入栈中:

> <esp,10/nap
0x8047330:
0x8047330: 0xd17fd900
0x8047334: 0x18
0x8047338: main+0x19
0x804733c: 3
0x8047340: libc.so.1`_sse_hw
0x8047344: libc.so.1`__flt_rounds
0x8047348: 0x804736c
0x804734c: 0xd17fd900
0x8047350: 0x18
0x8047354: main+0x19
0x8047358: 0x80506ec
0x804735c: 0x8047460
0x8047360: 0x8047354
0x8047364: 0xd17fb840
0x8047368: 0x8047460
0x804736c: 0x804738c
>

elf_rtbndr会返回我们需要的printf在libc.so中的绝对地址吗?

用mdb在ld.so.1`elf_rtbndr返回处设置断点,继续执行:

> ld.so.1`elf_rtbndr+0x14:b
> :c
mdb: stop at ld.so.1`elf_rtbndr+0x14
mdb: target stopped at:
ld.so.1`elf_rtbndr+0x14:addl $0xc,%esp

检查一下函数返回值,它应该存在rax的寄存器中:

> <eax=X
d1741f39

显然,d1741f39就是printf的绝对地址,它处于libc.so中:

> d1741f39::dis -w
libc.so.1`printf: pushl %ebp
libc.so.1`printf+1: movl %esp,%ebp
libc.so.1`printf+3: subl $0x10,%esp
libc.so.1`printf+6: andl $0xfffffff0,%esp
libc.so.1`printf+9: pushl %ebx
libc.so.1`printf+0xa: pushl %esi
libc.so.1`printf+0xb: pushl %edi
libc.so.1`printf+0xc: call +0x5 <libc.so.1`printf+0x11>
libc.so.1`printf+0x11: popl %ebx
libc.so.1`printf+0x12: addl $0x6d0b6,%ebx
libc.so.1`printf+0x18: movl 0x244(%ebx),%esi


此时此刻,GOT中的printf的对应项GOT[7],即0×8060714地址处,已经被ld.so修改成printf的绝对地址:

> 0x80606fc,9/nap
0x80606fc:
0x80606fc: 0x806071c
0x8060700: 0xd17fd900
0x8060704: ld.so.1`elf_rtbndr
0x8060708: libc.so.1`atexit
0x806070c: libc.so.1`_fpstart
0x8060710: PLT:exit
0x8060714: libc.so.1`printf
0x8060718: PLT:_get_exit_frame_monitor
0x806071c: 1
>

printf被成功解析后,ld.so修改了GOT[7],接着就应该把控制权转到libc的printf函数了。显然,在 ld.so.1`elf_rtbndr+0×17处的指令将会把eax寄存器中的printf的绝对函数地址存入栈中:

> ld.so.1`elf_rtbndr+0x17:b
> :c
mdb: stop at ld.so.1`elf_rtbndr+0x17
mdb: target stopped at:
ld.so.1`elf_rtbndr+0x17:movl %eax,0x8(%ebp)

此时栈中还没有printf的地址:

> <esp,10/nap
0x80473cc:
0x80473cc: 3
0x80473d0: libc.so.1`_sse_hw
0x80473d4: libc.so.1`__flt_rounds
0x80473d8: 0x80473fc
0x80473dc: 0xd17fd900
0x80473e0: 0x18
0x80473e4: main+0x19
0x80473e8: 0x80506ec
0x80473ec: 0x80474f4
0x80473f0: 0x80473e8
0x80473f4: 0xd17fb840
0x80473f8: 0x80474f4
0x80473fc: 0x8047420
0x8047400: _start+0x7a
0x8047404: 1
0x8047408: 0x804742c

单步执行后,再观察栈,会发现,printf已经存入栈:

> :s
mdb: target stopped at:
ld.so.1`elf_rtbndr+0x1a:popl %edx
> <esp,10/nap
0x80473cc:
0x80473cc: 3
0x80473d0: libc.so.1`_sse_hw
0x80473d4: libc.so.1`__flt_rounds
0x80473d8: 0x80473fc
0x80473dc: 0xd17fd900
0x80473e0: libc.so.1`printf
0x80473e4: main+0x19
0x80473e8: 0x80506ec
0x80473ec: 0x80474f4
0x80473f0: 0x80473e8
0x80473f4: 0xd17fb840
0x80473f8: 0x80474f4
0x80473fc: 0x8047420
0x8047400: _start+0x7a
0x8047404: 1
0x8047408: 0x804742c


在ld.so.1`elf_rtbndr返回的前一刻,printf恰好成为ld.so.1`elf_rtbndr的返回地址:

> :s
mdb: target stopped at:
ld.so.1`elf_rtbndr+0x23:ret
> <esp,10/nap
0x8047350:
0x8047350: libc.so.1`printf
0x8047354: main+0x19
0x8047358: 0x80506ec
0x804735c: 0x8047460
0x8047360: 0x8047354
0x8047364: 0xd17fb840
0x8047368: 0x8047460
0x804736c: 0x804738c
0x8047370: _start+0x7a
0x8047374: 1
0x8047378: 0x8047398
0x804737c: 0x80473a0
0x8047380: _start+0x1f
0x8047384: _fini
0x8047388: ld.so.1`atexit_fini
0x804738c: 0

这样,控制权就由ld.so到了我们要调用的函数 – printf:

> :s
mdb: target stopped at:
libc.so.1`printf: pushl %ebp

至此,一个完整的动态绑定过程结束,此时可以再次反汇编我们的main函数:

> main::dis
main: pushl %ebp
main+1: movl %esp,%ebp
main+3: subl $0x10,%esp
main+6: movl %ebx,-0x8(%ebp)
main+9: movl %esi,-0xc(%ebp)
main+0xc: movl %edi,-0x10(%ebp)
main+0xf: pushl $0x80506ec
main+0x14: call -0x148 <PLT=libc.so.1`printf>
main+0x19: addl $0x4,%esp
main+0x1c: movl $0x0,-0x4(%ebp)
main+0x23: jmp +0x5 <main+0x28>
main+0x28: movl -0x4(%ebp),%eax
main+0x2b: movl -0x8(%ebp),%ebx
main+0x2e: movl -0xc(%ebp),%esi
main+0x31: movl -0x10(%ebp),%edi
main+0x34: leave
main+0x35: ret
>

可以看到,由于GOT[7]已经存储了printf的绝对地址,因此,反汇编结果发生了变化。

进程第一次调用printf的动态解析的过程如下:


main
|
V
PLT:printf的第1条指令<---GOT[7]指向的地址
| |
V |
PLT:printf的第2条指令<---------+
|
V
PLT:printf的第3条指令
|
V
PLT0
ld.so.1`elf_rtbndr
|
V
libc.so.1`printf


如果该进程再次调用printf:


main
|
V
PLT:printf的第1条指令<---GOT[7]指向的地址
| |
V |
libc.so.1`printf<---------+



3. elf_bndr函数

elf_rtbndr在32bit x86平台的源代码的位置在:
link:http://cvs.opensolaris.org/source/xref/on/usr/src/cmd/sgs/rtld/i386/i386_elf.c

要实现动态绑定,elf_bndr应至少完成如下工作:

3.1 确定要绑定的符号

下面部分elf_bndr的代码就是根据重定位表来确定要绑定的符号:

   231     /*
232 * Use relocation entry to get symbol table entry and symbol name.
233 */
234 addr = (ulong_t)JMPREL(lmp);
235 rptr = (Rel *)(addr + reloff);
236 rsymndx = ELF_R_SYM(rptr->r_info);
237 sym = (Sym *)((ulong_t)SYMTAB(lmp) + (rsymndx * SYMENT(lmp)));
238 name = (char *)(STRTAB(lmp) + sym->st_name);
239


JMPREL,SYMTAB,SYMENT,STRTAB这些宏都能从函数第1个入口参数lmp指针,即Rt_map指针中得到下面elfdump中看到 的值:

# /usr/ccs/bin/elfdump -d test

Dynamic Section: .dynamic
index tag value
[0] NEEDED 0x111 libc.so.1
[1] INIT 0x80506b0
[2] FINI 0x80506cc
[3] HASH 0x80500e8
[4] STRTAB 0x805036c --->STRTAB(lmp)的值,字符串表基地址
[5] STRSZ 0x137
[6] SYMTAB 0x80501cc --->SYMTAB(lmp)的值,符号表基地址
[7] SYMENT 0x10 --->SYMENT(lmp)的值,符号表元素的长度
[8] CHECKSUM 0x5a2b
[9] VERNEED 0x80504a4
[10] VERNEEDNUM 0x1
[11] PLTRELSZ 0x28
[12] PLTREL 0x11
[13] JMPREL 0x80504dc --->JMPREL(lmp)的值,重定位表基地址
[14] REL 0x80504d4
[15] RELSZ 0x30
[16] RELENT 0x8
[17] DEBUG 0
[18] FEATURE_1 0x1 [ PARINIT ]
[19] FLAGS 0 0
[20] FLAGS_1 0 0
[21] PLTGOT 0x80606fc


因此,addr的值就是0×80504dc,它实际上是test进程的重定位表的地址。

reloff是第二个参数,在前面查找printf的过程中,我们知道它的值为0×18,因此rptr的值为:

rptr = addr + reloff = 0×80504dc + 0×18 = 80504f4

前面已经用mdb查看实际内存中的重定位表的内容:

# mdb test
> 0x80504dc,a/nap
0x80504dc:
0x80504dc: 0x8060708
0x80504e0: 0xf07
0x80504e4: 0x806070c
0x80504e8: 0x1007
0x80504ec: 0x8060710
0x80504f0: 0x1207
0x80504f4: 0x8060714
0x80504f8: 0x107
0x80504fc: 0x8060718
0x8050500: 0x1307

因此rptr->r_offset=0×8060714,rptr->r_info=0×107,实际上这个rptr就指向 printf在重定位表中的相应项,而rptr->r_offset就对应着printf在GOT中的的地址,即GOT[7]地址。

ELF_R_SYM这个宏实际上是向右位移8位,因此rsymndx的值实际上是:

rsymndx = ELF_R_SYM(rptr->r_info)= 0×107 << 8 = 1

sym就是printf的符号表中的记录:

sym = 0×80501cc + (1 * 0×10) = 0×80501dc

因此name的地址是,它指向printf字符串:

name = 0×805036c + 1 = 0×805036d


# mdb test
> 80501dc,2/nap
0x80501dc:
0x80501dc: 1 ---> sym->st_name
0x80501e0: PLT:printf ---> sym->st_value
> 0x805036d/s
0x805036d: printf ---> name的值
>

可见,根据给定符号对应的重定位表的偏移量,就可以找到该符号的符号表的记录,进而确定其名字字符串。

3.2 遍历所有依赖库的符号表查找给定符号

   244     llmp = LIST(lmp)->lm_tail;
245
246 /*
247 * Find definition for symbol.
248 */
249 sl.sl_name = name;
250 sl.sl_cmap = lmp;
251 sl.sl_imap = LIST(lmp)->lm_head;
252 sl.sl_hash = 0;
253 sl.sl_rsymndx = rsymndx;
254 sl.sl_flags = LKUP_DEFT;
255
256 if ((nsym = lookup_sym(&sl, &nlmp, &binfo)) == 0) {
257 eprintf(ERR_FATAL, MSG_INTL(MSG_REL_NOSYM), NAME(lmp),
258 demangle(name));
259 rtldexit(LIST(lmp), 1);
260 }
261


在256行的lookup_sym函数会根据传入的符号名和link map返回共享库中对应的符号表记录的指针nsym,&nlmp, &binfo是另外的两个返回值。因此,真正确定符号位置的关键参数就是sl参数了,其定义如下:

link:http://cvs.opensolaris.org/source/xref/on/usr/src/cmd/sgs/include/rtld.h


775 typedef struct {
776 const char *sl_name; /* symbol name */
777 Rt_map *sl_cmap; /* callers link-map */
778 Rt_map *sl_imap; /* initial link-map to search */
779 ulong_t sl_hash; /* symbol hash value */
780 ulong_t sl_rsymndx; /* referencing reloc symndx */
781 uint_t sl_flags; /* lookup flags */
782 } Slookup;
783


可以看到,sl中包含的信息主要有3类:

符号相关的:*sl_name,sl_hash,sl_rsymndx,唯一地确定符号,sl_hash将用于符号查找 linkmap: *sl_cmap, *sl_imap, 维护着依赖库加载、ld.so控制信息搜索控制标志: sl_flags,此标志直接影响下级调用的code path

要确定一个给定符号在哪一个依赖库,以及其在共享库的绝对地址,link map起着关键的作用,下面是Rt_map定义:

http://cvs.opensolaris.org/source/xref/on/usr/src/cmd/sgs/include/rtld.h:

   64 typedef struct rt_map    Rt_map;

459 struct rt_map {
460 /*
461 * BEGIN: Exposed to rtld_db - don't move, don't delete
462 */
463 Link_map rt_public; /* public data */
..................................................................................
485 struct fct *rt_fct; /* file class table for this object */
486 Sym *(*rt_symintp)(); /* link map symbol interpreter */
487 void *rt_priv; /* private data, object type specific */
488 Lm_list *rt_list; /* link map list we belong to */
..................................................................................
523 };

Rt_map的起始地址处定义了一个结构Link_map,它的定义如下:

   422 typedef struct link_map    Link_map;

422 typedef struct link_map Link_map;
423
424 struct link_map {
425 unsigned long l_addr; /* address at which object is mapped */
426 char *l_name; /* full name of loaded object */
427 #ifdef _LP64
428 Elf64_Dyn *l_ld; /* dynamic structure of object */
429 #else
430 Elf32_Dyn *l_ld; /* dynamic structure of object */
431 #endif
432 Link_map *l_next; /* next link object */
433 Link_map *l_prev; /* previous link object */
434 char *l_refname; /* filters reference name */
435 };

可以看到实际上多个Rt_map是可以通过双向链表链接起来。

下面用mdb来查看正在运行着的test的Rt_map,0xd17fd900就是解析printf时传递给elf_bndr的首地址:

> 0xd17fd900,20/nap
0xd17fd900:
0xd17fd900: 0x8050000
0xd17fd904: 0x8047ff5
0xd17fd908: 0x806071c
0xd17fd90c: 0xd17fdd40
0xd17fd910: 0
0xd17fd914: 0
0xd17fd918: 0xd17fdbe8
0xd17fd91c: 0x8050000
0xd17fd920: 0x10820
0xd17fd924: 0x10820
0xd17fd928: 0x20421605
0xd17fd92c: 0x602
0xd17fd930: 0
0xd17fd934: 0xd17fdb78
0xd17fd938: 0
0xd17fd93c: 0
0xd17fd940: 0
0xd17fd944: 0
0xd17fd948: 0
0xd17fd94c: 0xd16d00d8
0xd17fd950: 0
0xd17fd954: 0
0xd17fd958: 0
0xd17fd95c: 0x80506f9
0xd17fd960: ld.so.1`elf_fct
0xd17fd964: ld.so.1`elf_find_sym
0xd17fd968: 0xd17fda00
0xd17fd96c: ld.so.1`lml_main
0xd17fd970: 0xffffffff
0xd17fd974: 0
0xd17fd978: 0
0xd17fd97c: 0x1901

Rt_map结构的成员rt_fct是指向struct fct结构的指针,struct fct结构定义如下:


71 typedef struct fct {
72 int (*fct_are_u_this)(Rej_desc *); /* determine type of object */
73 ulong_t (*fct_entry_pt)(void); /* get entry point */
74 Rt_map *(*fct_map_so)(Lm_list *, Aliste, const char *, const char *,
75 int); /* map in a shared object */
76 void (*fct_unmap_so)(Rt_map *); /* unmap a shared object */
77 int (*fct_needed)(Lm_list *, Aliste, Rt_map *);
78 /* determine needed objects */
79 Sym *(*fct_lookup_sym)(Slookup *, Rt_map **, uint_t *);
80 /* initialize symbol lookup */
81 int (*fct_reloc)(Rt_map *, uint_t); /* relocate shared object */
82 Pnode *fct_dflt_dirs; /* list of default dirs to */
83 /* search */
84 Pnode *fct_secure_dirs; /* list of secure dirs to */
85 /* search (set[ug]id) */
86 Pnode *(*fct_fix_name)(const char *, Rt_map *, uint_t);
87 /* transpose name */
88 char *(*fct_get_so)(const char *, const char *);
89 /* get shared object */
90 void (*fct_dladdr)(ulong_t, Rt_map *, Dl_info *, void **, int);
91 /* get symbolic address */
92 Sym *(*fct_dlsym)(Grp_hdl *, Slookup *, Rt_map **, uint_t *);
93 /* process dlsym request */
94 int (*fct_verify_vers)(const char *, Rt_map *, Rt_map *);
95 /* verify versioning (ELF) */
96 int (*fct_set_prot)(Rt_map *, int);
97 /* set protection */
98 } Fct;


可以看到,这个结构中抽象出了一个二进制对象所有相关的操作函数表,根据二进制对象的类型,它可以实际动态绑定函数到不同类型的二进制文件操作函数上,这 种实现方式充分体现了操作系统中面向对象设计思想,这使得ld.so扩展新的可执行文件格式的支持变得相当容易。

ELF文件和a.out文件格式的相关代码分别如下,仅供参考:

http://cvs.opensolaris.org/source/xref/on/usr/src/cmd/sgs/rtld/common/elf.c http://cvs.opensolaris.org/source/xref/on/usr/src/cmd/sgs/rtld/common/a.out.c

用mdb检查test进程的操作函数,可以看到,由于test的类型是ELF文件,因此elf.c定义的函数表绑定到了rt_fct上:

> ld.so.1`elf_fct,10/nap
ld.so.1`elf_fct:
ld.so.1`elf_fct:
ld.so.1`elf_fct:ld.so.1`elf_are_u
ld.so.1`elf_fct+4: ld.so.1`elf_entry_pt
ld.so.1`elf_fct+8: ld.so.1`elf_map_so
ld.so.1`elf_fct+0xc: ld.so.1`elf_unmap_so
ld.so.1`elf_fct+0x10: ld.so.1`elf_needed
ld.so.1`elf_fct+0x14: ld.so.1`lookup_sym
ld.so.1`elf_fct+0x18: ld.so.1`elf_reloc
ld.so.1`elf_fct+0x1c: ld.so.1`elf_dflt_dirs
ld.so.1`elf_fct+0x20: ld.so.1`elf_secure_dirs
ld.so.1`elf_fct+0x24: ld.so.1`elf_fix_name
ld.so.1`elf_fct+0x28: ld.so.1`elf_get_so
ld.so.1`elf_fct+0x2c: ld.so.1`elf_dladdr
ld.so.1`elf_fct+0x30: ld.so.1`dlsym_handle
ld.so.1`elf_fct+0x34: ld.so.1`elf_verify_vers
ld.so.1`elf_fct+0x38: ld.so.1`elf_set_prot
ld.so.1`elf_secure_dirs: ld.so.1`__rtld_msg+0x133e

与rt_fct类似的是Rt_map的另一个成员,rt_symintp,它实际上指向了真正的符号解析函数elf_find_sym:

....................................
0xd17fd964: ld.so.1`elf_find_sym
....................................

正是elf_find_sym,完成了真正的符号表查找工作。

用mdb来遍历从0xd17fd900起始的Rt_map的双向链表:

> 0xd17fd900,6/nap
0xd17fd900:
0xd17fd900: 0x8050000 --->test加载地址
0xd17fd904: 0x8047ff5 --->Rt_map对应的二进制对象名,此处是test
0xd17fd908: 0x806071c
0xd17fd90c: 0xd17fdd40 --->后向指针,指向libc.so的link map
0xd17fd910: 0 --->前向指针,此处为NULL,表明是linkmap list的头
0xd17fd914: 0
> 0x8047ff5/s
0x8047ff5: test --->名字验证
> 0xd17fdd40,6/nap
0xd17fdd40:
0xd17fdd40: 0xd16e0000 --->libc.so加载地址
0xd17fdd44: 0xd17fdcd0 --->Rt_map对应的二进制对象名,此处是/lib/libc.so.1
0xd17fdd48: 0xd17afa3c
0xd17fdd4c: 0 ---->后向指针,是NULL,表明是linkmap list的尾
0xd17fdd50: 0xd17fd900 ---->前向指针,指向test的link map
0xd17fdd54: 0
> 0xd17fdcd0/s
0xd17fdcd0: /lib/libc.so.1

与可执行文件不同,共享库中并没有在ELF文件的.text section头中规定共享库的加载地址,而只是给出了相对地址,待被装载后才重新确定:

# /usr/ccs/bin/elfdump -c -N .text /usr/lib/libc.so

Section Header[11]: sh_name: .text
sh_addr: 0x1f370 sh_flags: [ SHF_ALLOC SHF_EXECINSTR ]
sh_size: 0x89895 sh_type: [ SHT_PROGBITS ]
sh_offset: 0x1f370 sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x10

而实际上,通过遍历linkmap list,ld.so可以确定所有linkmap list中的二进制对象的实际装载地址。

这里libc.so的实际地址是0xd16e0000,可以通过pmap(1)验证得到的地址是否正确:

# pmap -x 1597
1597: test
Address Kbytes RSS Anon Locked Mode Mapped File
08046000 8 8 8 - rwx-- [ stack ]
08050000 4 4 - - r-x-- test
08060000 4 4 4 - rwx-- test
D16C0000 24 12 12 - rwx-- [ anon ]
D16D0000 4 4 4 - rwx-- [ anon ]
D16E0000 764 764 - - r-x-- libc.so.1
D17AF000 24 24 24 - rw--- libc.so.1
D17B5000 8 8 8 - rw--- libc.so.1
D17C8000 140 140 - - r-x-- ld.so.1
D17FB000 4 4 4 - rwx-- ld.so.1
D17FC000 8 8 8 - rwx-- ld.so.1
-------- ------- ------- ------- -------
total Kb 992 980 72 -

同样的,共享库中符号表的st_value也不是该符号的绝对地址,而是偏移量,例如,libc.so中符号表中printf的取值是:

# /usr/ccs/bin/elfdump -s -N .dynsym /usr/lib/libc.so | grep " printf___FCKpd___70quot;
[2416] 0x00061f39 0x00000105 FUNC GLOB D 34 .text printf

那么,如果lookup_sym函数得到printf在libc.so中的符号表记录的指针,那么很容易计算得出printf的绝对地址。

本例中,共享库中printf在符号表中st_value的取值和libc.so的装载地址都已经确定了,因此printf的绝对地址是:

> 0xd16e0000+0x00061f39=X
d1741f39

如果用mdb反汇编这个地址,d1741f39就是printf在libc.so的真正入口:

> d1741f39::dis -w
libc.so.1`printf: pushl %ebp
libc.so.1`printf+1: movl %esp,%ebp
libc.so.1`printf+3: subl $0x10,%esp
libc.so.1`printf+6: andl $0xfffffff0,%esp
libc.so.1`printf+9: pushl %ebx
libc.so.1`printf+0xa: pushl %esi
libc.so.1`printf+0xb: pushl %edi
libc.so.1`printf+0xc: call +0x5 <libc.so.1`printf+0x11>
libc.so.1`printf+0x11: popl %ebx
libc.so.1`printf+0x12: addl $0x6d0b6,%ebx
libc.so.1`printf+0x18: movl 0x244(%ebx),%esi

前面我们遍历link map是从0xd17fd900开始的,这个地址指向的Rt_map节点碰巧是整个linkmap list的头节点。实际上,0xd17fd900指向的Rt_map的准确含义是调用者的link map,假设符号解析的调用是从共享库发出的,那么这个地址指向的Rt_map就未必是头节点了。

实际上,每个进程的Rt_map都指向一个全局变量lml_main,通过该变量即可找到这个进程完整的linkmap list.

Rt_map结构成员rt_list指针就指向lml_main全局变量,它实际上是Lm_list结构,定义如下:

   799 extern Lm_list        lml_main;    /* main's link map list */

Lm_list定义如下:

   239 typedef    struct {
240 /*
241 * BEGIN: Exposed to rtld_db - don't move, don't delete
242 */
243 Rt_map *lm_head; /* linked list pointers to active */
244 Rt_map *lm_tail; /* link-map list */
.....................................................................
263 } Lm_list;

这样,实际上通过rt_list->lm_head即可定位到进程的linkmap list的头节点了,elf_bndr函数就是这样做的:

   250     sl.sl_cmap = lmp;                 --->指向调用者的Rt_map
251 sl.sl_imap = LIST(lmp)->lm_head; --->取得进程的link map list的头节点

因此,要确定给定符号存在于哪一个依赖的共享库时,需要遍历所有linkmap list中的节点时,就需要使用sl.sl_imap。

实际上,ld.so为mdb提供了专门的命令,以方便与ld.so相关的数据结构的查看:

让test进程运行:

# mdb test
> main+0x14:b
> :c
mdb: stop at main+0x14
mdb: target stopped at:
main+0x14: call -0x148 <PLT:printf>

装载ld.so模块:

> ::load ld.so

查看目前ld.so管理的所有Rt_map:

> ::Rt_maps
Link-map lists (dynlm_list): 0x8046368
----------------------------------------------
Lm_list: 0xd17fb220 (LM_ID_BASE)
----------------------------------------------
lmco rtmap ADDR() NAME()
----------------------------------------------
[0xc] 0xd17fd900 0x08050000 test
[0xc] 0xd17fdd40 0xd16e0000 /lib/libc.so.1
----------------------------------------------
Lm_list: 0xd17fb1e0 (LM_ID_LDSO)
----------------------------------------------
[0xc] 0xd17fd590 0xd17c8000 /lib/ld.so.1


只查看test进程的Rt_maps列表:

> 0xd17fd900::Rt_maps -v
----------------------------------------------
Rt_map located at: 0xd17fd900
----------------------------------------------
NAME: test
PATHNAME: /export/home/personal/blog/test
ADDR: 0x08050000 DYN: 0x0806071c
NEXT: 0xd17fdd40 PREV: 0x00000000
FCT: 0xd17fb054 TLSMODID: 0
INIT: 0x00000000 FINI: 0x00000000
GROUPS: 0x00000000 HANDLES: 0x00000000
DEPENDS: 0xd16d00d8 CALLERS: 0x00000000
DYNINFO: 0xd17fda80 REFNAME:
RLIST: 0x00000000 RPATH:
LIST: 0xd17fb220 [ld.so.1`lml_main]
FLAGS: 0x20421605
[ ISMAIN,RELOCED,ANALYZED,INITDONE,FIXED,MODESET,INITCALL,INITCLCT ]
FLAGS1: 0x00000602
[ RELATIVE,NOINITFINI,USED ]
MODE: 0x00001901
[ LAZY,GLOBAL,WORLD,NODELETE ]
----------------------------------------------
Rt_map located at: 0xd17fdd40
----------------------------------------------
NAME: /lib/libc.so.1
ADDR: 0xd16e0000 DYN: 0xd17afa3c
NEXT: 0x00000000 PREV: 0xd17fd900
FCT: 0xd17fb054 TLSMODID: 0
INIT: 0xd1788c10 FINI: 0xd1788c30
GROUPS: 0x00000000 HANDLES: 0x00000000
DEPENDS: 0xd16d02e0 CALLERS: 0xd16d0120
DYNINFO: 0xd17fdee0 REFNAME:
RLIST: 0x00000000 RPATH:
LIST: 0xd17fb220 [ld.so.1`lml_main]
FLAGS: 0x20420604
[ RELOCED,ANALYZED,INITDONE,MODESET,INITCALL,INITCLCT ]
FLAGS1: 0x00004402
[ RELATIVE,USED,SYMSFLTR ]
MODE: 0x00001901
[ LAZY,GLOBAL,WORLD,NODELETE ]


查看test的Rt_map对用的Lm_list结构:

> 0xd17fb220::Lm_list
Lm_list: 0xd17fb220 (LM_ID_BASE)
----------------------------------------------
lists: 0xd17fd3f0 Alist[used 1: total 8]
----------------------------------------------
head: 0xd17fd900 tail: 0xd17fdd40 ---->可以看到,这里有link map list的头尾节点指针
audit: 0x00000000 preexec: 0xd17fdd40
handle: 0x00000000 obj: 2 init: 0 lazy: 0
flags: 0x00000821
[ BASELM,ENVIRON,STARTREL ]
tflags: 0x00000000
>

不难想象,顺序遍历linkmap list,查找当前库是否包含printf符号,如果包含就返回指向符号表记录的指针,这就是lookup_sym接下来要做的工作。

3.3 算出符号绝对地址,并存储到GOT中该符号的对应项中


下面的代码相当容易理解:

   262     symval = nsym->st_value;
263 if (!(FLAGS(nlmp) & FLAG_RT_FIXED) &&
264 (nsym->st_shndx != SHN_ABS))
265 symval += ADDR(nlmp);

symval即printf在libc.so的符号表的st_value。nlmp则返回包含printf的libc的指向Rt_map指针的指针。

263行是保证包含给定符号库是不是固定地址映像的二进制文件,FLAGS(nlmp)可以从返回的Rt_map中得到二进制对象的类型。 264行则是判断取得的符号的类型是不是绝对地址。

libc.so是共享库,因此,最终运行到265行,将st_value与ADDR(nlmp),即libc的基地址相加,得出绝对地址。


下面的代码会把printf的绝对地址存储到GOT[7]中,因此首先要得到GOT[7]的地址:

   281     if (!(rtld_flags & RT_FL_NOBIND)) {
282 addr = rptr->r_offset;

在3.1小节,我们已经知道rptr->r_offset就对应着printf在GOT中的的地址,即GOT[7]地址。

下面对addr的改变只发生在当前调用者的Rt_map,即0xd17fd900指向的Rt_map,不是固定影射的二进制对象,我们知道test文件是 固定影射的,因此下面2条语句在printf解析时,根本不会执行:

   283         if (!(FLAGS(lmp) & FLAG_RT_FIXED))
284 addr += ADDR(lmp);

最终,304行的语句会将printf的绝对地址存入GOT[7]中:

   285         if (((LIST(lmp)->lm_tflags | FLAGS1(lmp)) &
286 (LML_TFLAG_AUD_PLTENTER | LML_TFLAG_AUD_PLTEXIT)) &&
287 AUDINFO(lmp)->ai_dynplts) {
..............................................................................
..............................................................................
..............................................................................
299 } else {
300 /*
301 * Write standard PLT entry to jump directly
302 * to newly bound function.
303 */
304 *(ulong_t *)addr = symval;
305 }
306 }


4. lookup_sym -> _lookup_sym -> elf_find_sym

实际上,为了提高在符号表中查找符号的效率,ELF文件中包含了一个.hash section,可以利用其中的hash表来进行符号查找:

# /usr/ccs/bin/elfdump -h test

Hash Section: .hash
bucket symndx name
0 [1] printf
1 [2] environ
[3] _PROCEDURE_LINKAGE_TABLE_
3 [4] _DYNAMIC
5 [5] _edata
[6] ___Argv
6 [7] _etext
[8] _init
7 [9] __fsr_init_value
9 [10] main
[11] _mcount
10 [12] _environ
11 [13] _GLOBAL_OFFSET_TABLE_
15 [14] _lib_version
16 [15] atexit
[16] __fpstart
18 [17] __fsr
[18] exit
[19] _get_exit_frame_monitor
19 [20] _end
[21] _start
21 [22] _fini
24 [23] __environ_lock
27 [24] __longdouble_used
28 [25] __1cG__CrunMdo_exit_code6F_v_

12 buckets contain 0 symbols
10 buckets contain 1 symbols
6 buckets contain 2 symbols
1 buckets contain 3 symbols
29 buckets 25 symbols (globals)

ELF文件的.hash section提供了hash表本身,以及hash表元素的数目即nbuckets,每个hash表的bucket可能对应一个chain,chain的每一个元素是下一个符号在字符串表中的索引,这样这个chain相当于一个字符串索引值组成的list。这样,给定一个符号名,通过ELF规范定义的 hash函数,可以求得一个bucket号,再根据bucket号,遍历其对应的chain,对比字符串,来查找符号:

1. hn = elf_hash(sym_name) % nbuckets; 
2. for (ndx = hash[ hn ]; ndx; ndx = chain[ ndx ]) {
3. symbol = sym_tab + ndx;
4. if (strcmp(sym_name, str_tab + symbol->st_name) == 0)
5. return (load_addr + symbol->st_value); }

利用mdb,我们可以得到完整的解析printf时的代码路径:

bash-3.00# mdb test
> main+0x14:b
> :c
mdb: stop at main+0x14
mdb: target stopped at:
main+0x14: call -0x148 <PLT:printf>
> ld.so.1`elf_find_sym::dis !grep strcmp
ld.so.1`elf_find_sym+0xbf: call +0x14e14 <ld.so.1`strcmp>
> ld.so.1`elf_find_sym+0xbf:b
> :c
mdb: stop at ld.so.1`elf_find_sym+0xbf
mdb: target stopped at:
ld.so.1`elf_find_sym+0xbf: call +0x14e14 <ld.so.1`strcmp>
> $c
ld.so.1`elf_find_sym+0xbf(80472e8, 80473ac, 80473b0)
ld.so.1`_lookup_sym+0x6e(d17fd900, 80472e8, 80473ac, 80473b0, c)
ld.so.1`lookup_sym+0x1d7(8047358, 80473ac, 80473b0)
ld.so.1`elf_bndr+0xf8(d17fd900, 18, 8050691)
ld.so.1`elf_rtbndr+0x14(18, 8050691, 80506ec, 80474f4, 80473e8, d17fb840)
0xd17fd900(1, 804742c, 8047434)
_start+0x7a(1, 804755c, 0, 8047561, 8047583, 8047597)
>

lookup_sym函数根据给定的符号名,通过hash函数算出其在hash表中的bucket号:


2492 if (slp->sl_hash == 0)
2493 slp->sl_hash = elf_hash(name);

_lookup_sym中循环遍历了linkmap list,对每个依赖库调用了SYMINTP来解析符号:

  2438     for (; lmp; lmp = (Rt_map *)NEXT(lmp)) {
2439 if (callable(slp->sl_cmap, lmp, 0)) {
2440 Sym *sym;
2441
2442 slp->sl_imap = lmp;
2443 if ((sym = SYMINTP(lmp)(slp, dlmp, binfo)) != 0)
2444 return (sym);
2445 }
2446 }

如果是ELF文件,SYMINTP对应的则是elf_find_sym函数,它在给定ELF对象的指定bucket中的chain list来查找符号。

查找对比符号必然要调用strcmp函数,因此我们可以利用dtrace脚本来观察这种比较是如何进行的:

#!/usr/sbin/dtrace -s
#pragma D option quiet
BEGIN
{
printf("Target pid: %d\n", $target);
}
pid$target::main:entry
{
self->main=1;
}
pid$target::main:return
{
self->main=0;
}
pid$target::elf_find_sym:entry
/self->main==1/
{
self->trace=1;
}
pid$target::elf_find_sym:return
/self->main==1 && self->trace==1 /
{
self->trace=0;
}
pid$target::strcmp:entry
/self->main==1 && self->trace==1 /
{
printf("\n%s`%s(%s,%s)\n", probemod, probefunc,copyinstr(arg0),copyinstr(arg1));
}

运行dtrace脚本来观察每次elf_find_sym调用strcmp时的入口参数:

# ./test.d -c ./test
hello world
Target pid: 3934
LM1`ld.so.1`strcmp(rintf,rintf)
LM1`ld.so.1`strcmp(rintf,rintf)
LM1`ld.so.1`strcmp(edata,findbuf)
LM1`ld.so.1`strcmp(__Argv,findbuf)
..............................................


可以看到,strcmp在查找printf时只对比了rintf而不是printf,这是为什么呢?查看代码可以找到答案:

  1869         if ((*strtabname++ != *name) || strcmp(strtabname, &name[1])) {
1870 if ((ndx = chainptr[ndx]) != 0)
1871 continue;
1872 return ((Sym *)0);
1873 }
1874

1869行代码是一个语言或表达式,首先比较两个字符串的首字符,如果不相等,则或表达式已经为真,接下来的strcmp就不会被执行。这样做,可以减低 符号查找时带来的调用strcmp的开销。


相关文档:

EXECUTABLE AND LINKABLE FORMAT (ELF)
Linker and Libraries Guide
ELF 动态解析符号过程(修订版)
X86汇编语言学习手记(3)
Solaris学习笔记(2)
阅读笔记:库绑定 – 我们应该让它更精确一些


Technorati Tag:
Technorati Tag:


2006年01月01日


What’s your resolution for 2006?

作者: Badcoffee
Email:
blog.oliver@gmail.com
Blog: http://blog.csdn.net/yayong
2006年1月1日

In 2005, I try my best to do following things:

1. Lose weight

Health always is most important to us.
Although I made a little progress on losing weight, but it is still not enough.
I have to improve it in next year.

Low calorie, do aerobics everyday!

2. To be a good husband

Who can tell me how to be a good husband?

May be I’ll trying to become sensitive and romantic… ;)


3. To be a good engineer

I’m not sure which way is easy? A kernel hacker or a good engineer?

Let me fulfill my dream, I wanna both of them!

Following fields need me to be familiar:

  IA-32 architecture
  Solaris kernel & driver
  Linux Kernel & driver

I will continue to learn and practise them by writing blogs, participating in activies of opensolaris&linux community…

That’s all. Almost same with last year. haha…. :)

What’s your resolution for 2006, my friends?

2005年10月22日

OpenSolaris北京用户组的第一次活动

作者: Badcoffee
Email: blog.oliver@gmail.com
Blog: http://blog.csdn.net/yayong
2005年10月

10月15号,OpenSolaris北京用户组在北京西郊宾馆会议厅组织了成立以来的第一次活动。尽管OpenSolaris早在2005年6月14日就正式开放源代码,但国内开展这样的活动还是第一次。

下面是一些照片,因为没料到来了这么多人,会场又是这么大,所以随身携带的超薄数码相机表现不佳。还好,在OpenSoalris北京用户组邮件列表里找到的几张照片,加上我自己的,勉强够用吧 :)



会议开始,有半小时的自由交流时间,很遗憾因为人太多,没有机会认识新朋友。



演讲开始,礼堂很大,让人有开报告会的错觉,呵呵。



看看听众的反应…



Sun中国工程研究院院长王兴耀,演讲题目是OpenSolaris能给中国带来什么?



Erik Nordmark,sun的Distinguished Engineer,演讲题目是Solaris Networking。
这位老兄是瑞典人,后来到了美国,上台时戏称自己的英文是Swedish,我当时就听到台下有人低呼,看来Swedish English确实难懂啊 :)

下面是转帖自SUN中国技术社区的Erik 的简历,这才是真大牛啊

Erik
has been at Sun since 1989. He has spent most of this time on
networkingcentered around IP such as IP Multicast, IPv6, Web
performance, TCP/IPmultithreading, STREAMS, as well as significant
technical work in the Internet Engineering Task Force. The IETF work
has included working on many parts of IPv6 and serving for five years
as an IETF Area Director and as such a member of the Internet
Engineering Steering Group (IESG). Currently the IETF work includes
Scalable IPv6 Multihoming (shim6), Detecting Network Attachment (DNA),
and Transparent Interconnection of Lots of Links (TRILL).

Before
he joined Sun, Erik worked at the Swedish Institute of Computer
Science. He holds degrees from Linkoping University, Stanford
University, and Uppsala University.



提问时间…

对会议具体内容感兴趣的朋友,可以到SUN中国技术社区下载演讲的幻灯片。这里向对网络技术感兴趣的朋友推荐Erik同志的演讲,内容还是很有趣地:)

PS: StarSuite格式的文档在linux下可以用Open Office打开。

OpenSolaris能给中国带来什么?
主讲:Sin-Yaw Wang
演讲资料下载:PDF格式


Solairs中的网络
主讲:Erik Nordmark
演讲资料下载:PDF格式


如果有朋友对OpenSolaris北京用户组OpenSolaris中文用户组感兴趣的话,可以通过订阅邮件列表来加入进来。北京用户组主要是定期在北京举办一些网下的面对面的技术交流,而中文用户组则是OpenSoalris的中文社区,欢迎大家用中文交流。

相关链接:

OpenSolaris中文社区:

2005年10月16日

九寨 -  天堂

作者: Badcoffee
Email: blog.oliver@gmail.com
Blog: http://blog.csdn.net/yayong
2005年10月

    中国.四川.阿坝自治州。
    该如何形容九寨的美呢?
    语言、文字、甚至影像,在绝美的景色面前都是苍白的。