免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
123下一页
最近访问板块 发新帖
查看: 7884 | 回复: 21
打印 上一主题 下一主题

关于“linux共享库内部plt调用”澄清几个问题,继续讨论 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2009-01-04 10:43 |只看该作者 |倒序浏览
澄清1:linux 2.6.23,intel x86 32位平台,程序和共享库都是elf格式。
澄清2:我是想弄清楚共享库自己内部的plt调用问题,因为共享库内部的函数可以互相调用,他们的调用也是通过共享库的plt实现。不是想讨论应用程序的plt调用。我给出的程序段是个共享库,sum()和_sum()都是共享库里的函数,plt是共享库自己的plt,不是应用程序的plt,sum()调用_sum()。

关于cjaizss,mik和system888net的回复,继续提问。

1、cjaizss: 代码被编译为PIC的,但PIC和这个call的地址是什么没有必然直接关系。这个叫符号重定位(relocation)。man elf

我的问题是,这个plt表和sum()函数中代码的都是共享库自己的,尤其是sum()中调用_sum()的call指令,是在各应用程序中共享的。“call + 操作数”,这是一条指令,就好像“mov a, 1”一样,这条指令连同操作数是被编译成一个机器代码放在代码段。共享库装载的时候,loader会对符号进行重定位,也就是说,“loader会修改这条机器代码”(因为,原来是在call 378,现在映射到两个不同的程序中后要分别修改成call 0x00111378和call 0x00122378,对吗?),使call到正确的地址。那么,这条被修改的机器代码是在共享库的代码段,如果对不同的程序有不同的修改,这条指令还能共享吗?如果不能共享,那共享库所谓的共享代码还有意义吗?

我就是想知道,共享库的代码是被各程序共享的,可是像这种需要重定位符号的代码怎么实现共享的呢?重定位之后,call的操作数不同了,“call+操作数”这条指令翻译后的机器指令也不同了,怎么能共享呢?这可能是我对“什么是PIC代码”不太了解。能推荐点儿资料吗?

2、system888net: 对于有虚拟内存管理的linux系统来讲:这是正常的.每个进程都拥有自己的程序地址空间,可以是不同的.

对应用程序来讲,你说的没有问题,应用程序调用共享库的过程,使用的是应用程序的plt,这个没错。共享库内部的函数互相调用,使用的是共享库自己的plt,不是应用程序的plt。共享库的代码被各程序共享,但是对于共享库符号重定位后,有些指令需要修改,这些修改后的指令在各程序中各不相同,因为他们的操作数(符号地址)不同了,但是这些被修改后的指令还能共享吗?

3、mik班主给出了应用程序调用共享库的got、plt使用过程,非常清楚。共享库内部的函数互相调用,共享库被映射到各进程后,代码部分一些指令要进行符号重定位,重定位后,受影响的机器代码(例如 call + 操作数)应该各不相同了,他们还在被共享吗?如果不再共享,也就是说,共享库里的代码映射到各进程后,进行符号重定位,因为要修改机器代码,那么各程序都要分配物理页保存修改的代码,那么这么一来共享库的代码就不再是“共享”的了。斑竹对这个有研究吗,或者推荐一些资料?

多谢各位,继续讨论。

[ 本帖最后由 detian 于 2009-1-4 11:00 编辑 ]

论坛徽章:
0
2 [报告]
发表于 2009-01-04 11:36 |只看该作者
还是这个例子:
int main()
{
        printf("hello\n");
}


但是: gcc -c t.c
看看生成的 t.o :
00000000 <main>:
   0:        8d 4c 24 04                  lea    0x4(%esp),%ecx
   4:        83 e4 f0                     and    $0xfffffff0,%esp
   7:        ff 71 fc                     pushl  0xfffffffc(%ecx)
   a:        55                           push   %ebp
   b:        89 e5                        mov    %esp,%ebp
   d:        51                           push   %ecx
   e:        83 ec 04                     sub    $0x4,%esp
  11:        c7 04 24 00 00 00 00         movl   $0x0,(%esp)
  18:        e8 fc ff ff ff               call   19 <main+0x19>
  1d:        83 c4 04                     add    $0x4,%esp
  20:        59                           pop    %ecx
  21:        5d                           pop    %ebp
  22:        8d 61 fc                     lea    0xfffffffc(%ecx),%esp
  25:        c3                           ret   
Disassembly of section .rodata:

00000000 <.rodata>:
   0:        68 65 6c 6c 6f               push   $0x6f6c6c65


这是个 object 文件,未经链接的。
注意两个红色标注的地方


1、18:        e8 fc ff ff ff               call   19 <main+0x19>

这个 call 指令是未经链接时的情形,gcc 生成的未经链接的调用一般都是:call myself  也就是 call 自已本身



2、 0:        68 65 6c 6c 6f               push   $0x6f6c6c65

这是个字符串常量值:68 65 6c 6c 6f  --->  "hello", 这个常量值也是未经链接时的情形


----------------------------------------------------------------------------------------------------------------------------------

现在关键是 gcc 链接后会怎样生成 elf 可执行文件 ?

接器 ld 生成 elf 时主要工作:
1、每个函数生成 plt 表,如: puts@plt 表
2、生成 got 表,在 got 中放置 plt 指针 和 ld-linux.so.2 动态加载器入口
3、由 ld-linux.so.2 动态加载器去动态加载 puts() 函数的实体进入 address space 的 共享代码区。
4、ld-linux.so.2 的工作是在使用某个函数时,才会去加载函数进入共享区,那么它必须要有这个函数的 ID,或值入口之类的




SO.................

1、 e8 fc ff ff ff               call   19 <main+0x19>  这条语句会变为:

call <puts@plt>_offset, 这个 <puts@plt>_offset 是 elf 文件的 puts@plt 的偏移,也就是在 elf 寻找 puts@plt

即实例中的:
804836c:        e8 0b ff ff ff               call   804827c <puts@plt>

这里 0xffffff0b 就是 puts@plt 基于 eip 的偏移量


2、看看 ld-linux.so.2 是如何标识 puts() 的:

0804826c <puts@plt-0x10>:
804826c:        ff 35 48 95 04 08            pushl  0x8049548
8048272:        ff 25 4c 95 04 08            jmp    *0x804954c


这里 pushl 0x8049548 是作为参数传递给 ld-linux.so.2 ,估计是个指针,在 [0x8049548] 应该存放 puts() 的标识。
而 [0x804954c] 是存放 ld-linux.so.2 的入口地址,它接着跳转到 ld-linux.so.2



3、看看 "hello\n" 常量被链接后的情形:

8048365:        c7 04 24 58 84 04 08         movl   $0x8048458,(%esp)
804836c:        e8 0b ff ff ff               call   804827c <puts@plt>


puts("hello\n");   这里 0x8048458 这个值就是 "hello\n" 的指针值。


再看看 0x8048458 :
08048454 <_IO_stdin_used>:
8048454:        01 00                        add    %eax,(%eax)
8048456:        02 00                        add    (%eax),%al
8048458:        68 65 6c 6c 6f               push   $0x6f6c6c65


t.o 中的常量值被链接到此处,它的值就是 68 65 6c 6c 6f "hello"

论坛徽章:
0
3 [报告]
发表于 2009-01-04 11:51 |只看该作者
用户程序空间层:
----------------------------------------------------------------------------------
user_code1                 user_code2                    user_code3
     |                                   |                                      |              
     |                    |                         |
----------------------------------------------------------------------------------
                                 shared 空间:

libc 库:printf() 等
----------------------------------------------------------------------------------

每个用户代码有它们自已的 got 和 plt 表格,或许位置相同或许不同,但是 ld-linux.so.2 入口应该是相同的。
每个用户码对于 pic 处理手法都一样。

论坛徽章:
0
4 [报告]
发表于 2009-01-04 12:04 |只看该作者
原帖由 detian 于 2009-1-4 10:43 发表
澄清1:linux 2.6.23,intel x86 32位平台,程序和共享库都是elf格式。
澄清2:我是想弄清楚共享库自己内部的plt调用问题,因为共享库内部的函数可以互相调用,他们的调用也是通过共享库的plt实现。不是想讨论 ...


赞一个LZ这种钻研的精神。

论坛徽章:
0
5 [报告]
发表于 2009-01-04 12:56 |只看该作者
原帖由 detian 于 2009-1-4 10:43 发表
澄清1:linux 2.6.23,intel x86 32位平台,程序和共享库都是elf格式。
澄清2:我是想弄清楚共享库自己内部的plt调用问题,因为共享库内部的函数可以互相调用,他们的调用也是通过共享库的plt实现。不是想讨论 ...




lz是关心在同一个动态库中两个函数之间调用的时候plt修改是否会共享吗? 因为没见到你的代码,所以只能这么理解了,若理解有误请LZ更正.

对于sum call _sum,虽然两个函数都在动态库中,但运行时都是从属与某个进程的地址空间的,也即可以共享物理上的代码段,但不可能共享got和plt,因此所谓修改了plt符号表严格意义上来说是修改了用户空间的数据,比如_sum@plt,如果这段代码真的在物理共享段中,那要么为每个进程维护一个表项,要么就出错玩完.

论坛徽章:
0
6 [报告]
发表于 2009-01-04 15:11 |只看该作者
mik地解释已经非常清楚了,推荐lz先看看linker and loader 对连接器和加载器有个初步的认识.
对于obj 中
e8 fc ff ff ff  , fffffffc 是-4 为了连接器计算的时候更方便 这是汇编器把这里放这个数的原因之一.对于外部全局变量 放的就是0x00000000 对于gcc 别的不一定.

论坛徽章:
0
7 [报告]
发表于 2009-01-04 15:24 |只看该作者

回复 #2 mik 的帖子

接着斑竹的看。
如果把斑竹的main改成我的sum(),偏移18处的call不变,然后把t.c编程一个so库libt.so。请问,libt.so被映射到两个不同的进程中后(并且,映射的起始地址不同),每个进程都要对t.o进行重定位,那么,重定位后偏移18处的call指令变成什么样的了?

理论上讲,这个call指令是共享库的代码,应该被各进程共享,比如说,要么是call 0x00111018,要么是call 0x00122018,因为是共享的嘛,该指令只可能一是个呀。

但是,奇怪的是,我在调试的时候,返回编出来偏移18处的代码在各进程中竟然是不同的,也就是说,在进程1中是call 0x00111018,进程2中是call 0x00122018,如何解释?

论坛徽章:
0
8 [报告]
发表于 2009-01-04 15:30 |只看该作者
原帖由 ice-pl 于 2009-1-4 15:11 发表
mik地解释已经非常清楚了,推荐lz先看看linker and loader 对连接器和加载器有个初步的认识.
对于obj 中
e8 fc ff ff ff  , fffffffc 是-4 为了连接器计算的时候更方便 这是汇编器把这里放这个数的原因之一.对 ...



我的问题是,如果这条指令是共享库中的代码,它被映射到各进程后,这条指令在各进程中是相同的,还是不同的?

如果相同,那call的目的地址在各进程中不一定相同,则该call指令一定会出错;
如果不同,那共享库的代码就不是在各进程中共享了。我就关心这个“共享库中的call”(不是应用程序中的call),重定位以后成什么样了。

多谢回复,请继续。

论坛徽章:
0
9 [报告]
发表于 2009-01-04 15:41 |只看该作者
原帖由 system888net 于 2009-1-4 12:56 发表




lz是关心在同一个动态库中两个函数之间调用的时候plt修改是否会共享吗? 因为没见到你的代码,所以只能这么理解了,若理解有误请LZ更正.

对于sum call _sum,虽然两个函数都在动态库中,但运行时都是从属 ...


对,我关心的就是同一个共享库中的两个函数之间的调用问题。
看我的代码。

mylib.c

#include <stdio.h>
int a,aa;
int sum(int,int);
int _sum(int,int);

int sum(int x, int y)
{
  a = 1;
  aa =2 ;
  a = _sum(x,y);
  printf("in sum, a =%d\n",a);
  return a;
}
int _sum(int x, int y)
{
  return x + y;
}

注意:
1、mylib.c生成libmylib.so
2、注意语句“a=_sum(x,y);”,返回编后是“4af:   e8 c4 fe ff ff          call   378 <_sum@plt>”。请注意,这个“call 378”在被映射到不同的进程后应该是call不同的地址,因为共享库自己的plt表项_sum@plt的地址在各进程中是不同的(因为共享库被映射的起始地址不同)。那么,如果这一句call在各进程中是不同的,那岂不是共享库的代码并没有被各进程共享?

论坛徽章:
0
10 [报告]
发表于 2009-01-04 16:47 |只看该作者
我已经告诉过你了你需要去看看书 搞清楚概念 :


共享库和程序
在几乎所有能够同时运行多个程序的系统中,每个程序都有一套独立的页面,使各自
都有一个逻辑上独立的地址空间。由于错误或恶意的程序无法破坏或窃取其它程序信息,这
就使得系统更加的健壮,但也会带来性能问题。如果单一的程序或单一的程序库在多于一个
的地址空间中被使用,若能够在多个地址空间中共享这个程序或程序库的单一副本,那将节
省大量的内存。对于操作系统实现这个功能是相当简捷的——只需要将可执行程序文件映射
到每一个程序的地址空间即可。不可重定位的代码和只读的数据以RO方式映射,可写的数
据以COW方式映射。操作系统还可以让所有映射到该文件的进程之间共享RO和尚未被写的C
OW数据对应的物理页框(如果代码在加载时需要重定位,重定位过程会修改代码页,那他
们就必须被当作COW对待,而不是RO)。
要完成这种共享工作需要链接器予以相当多的支持。在可执行程序中,链接器需要将
所有的可执行代码聚集起来形成文件中可以被映射为RO的部分,而数据是可以被映射为COW
的另一部分。每一个段的开始地址都需要以页边界对齐,这既针对逻辑上的地址空间也包括
实际的被映射文件。当多个不同程序使用一个共享库时,链接器需要做标记,好让程序启动
时共享库可以被映射到它们各自的地址空间中。
位置无关代码
当一个程序在多个不同的地址空间运行时,操作系统通常可以将程序加载到各地址空
间的相同位置。这样可以让链接器将程序中所有的地址绑定到固定的位置且在程序加载时不
需要进行重定位,因此链接器的工作简单了很多。
共享库使情况变得相当复杂。在一些简单的共享库设计中,每一个库会在系统引导时
或库被建立时分配一个全局唯一的内存地址。这可以让每一个库放置在固定的位置上,但由
于库内存地址的全局列表需要由系统管理员维护这就给共享库的管理带来了严重的瓶颈。再
进一步,如果一个库的新版本比之前的版本尺寸大且无法保存在先前分配的位置,那么整个
的共享库,以及引用这些库的程序都需要被重新链接。
有一个替代的办法就是允许不同的程序将库映射到各自地址空间的不同位置。这会使
库的管理容易一些,但是这需要编译器、链接器和程序加载器的配置,好让库可以在工作的
时候忽略掉它被加载到地址空间的什么位置。
一个简单的方法是在库中包含标准的重定位信息,在库被映射到各个地址空间时,加
载器可以修改程序中的任何重定位地址以反映库被加载的位置。不幸的是,修改的过程会导
致对库的代码和数据的修改,这意味着若它是按照COW方式映射的则对应的页不能再被共享,
或它是按照RO方式映射的则会导致程序的崩溃。
为了避免这种情况,共享库使用了位置无关代码(PIC:Position Independnet Code),
这是无论被加载到内存中的任何位置都可以正常工作的代码。共享库中的代码通常都是位置
无关代码,这样代码可以以RO方式映射。数据页仍然带有需要被重定位的指针,但由于数
据页将以COW方式映射,因此这里对共享不会有什么损失。
对于大部分计算机系统,位置无关代码是非常容易创建的。本章中讨论的三种体系结
构都使用相对跳转,因此例程中的跳转指令无需重定位。对栈上的本地数据引用是基于基址
寄存器的相对寻址,因此也不需重定位。仅有的挑战在于对那些不在共享库中的例程的调用,
以及对全局数据的引用。直接数据寻址和SPARC的高位/低位寄存器加载技术是不能使用的,
因为他们都需要运行时重定位。幸运的是,还有很多方法可以用来处理库间调用和全局数据
引用。当我们在第9章和第10章讨论共享库的时候再对其详细讨论。
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP