PIC的实现

本文深入探讨了PIC编程中库、链接器和编译器的角色。通过解析表格和字节码,揭示了如何有效地管理和组织代码,以实现高效的小型控制器程序。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

今天研究了下PIC,记录下。

1)什么是PIC,为什么要PIC

PIC,即Position independent code,直接翻译就是位置无关代码,简单的说就是这个代码可以被load到内存的任意位置而不用做任何地址修正。这里指的是代码段,数据段可能需要地址修正。
PIC是share library机制的基础之一,要实现library在各个process之间可以share,代码必须是PIC。为什么?有了load时的relocation还不够么?答案是也可以够,但是仅仅有load时的relocation,基本等于没有用。share library的主要目的是让各个process可以共享common的代码(指代码段),这部分代码只在内存中占用一次内存,所有process共享这部分代码,而不需要每个process都有一份拷贝。因为代码段是要被share的,所以代码段的内容不能被改变。load时的relocation是在load时对代码做地址修正,所以一旦library被load了,这个library在所有共享该library的process的地址空间中的位置也确定了。有人会问这样有什么问题么?OS在第一次load这个library的时候完全可以找个available的地址空间阿,因为第一次load的时候,是可以被load到内存的任意地方的,所以只要有地址空间就可以。遗憾的是,这种情况下地址空间很可能不够,而导致没法load。考虑这样的问题,如果某人写了个捣蛋的library,占用很大的地址空间,一旦这个library被load了,就会导致其他library不能被load,因为已经没有available的地址空间了,这样这个系统就会崩溃。所以要实现share library,仅仅load时的relocation是不够的,我们需要一种机制,可以让library被load进process的任意地址空间,或者说library在不同的process中,可以被load到不同的地址空间,然后在OS层,通过OS的地址空间映射,来实现library在物理内存上的share。所以PIC是必须的。

2)怎么实现PIC

PIC需要解决的问题就是找到一种办法,避免load时的地址修正(relocation)。以下面的代码为例,该代码把内存中符号one_dword对应的地址的一个dword的内容放到%eax里。如果一个library包含下面的代码,则这个代码不是PIC的。因为$one_byte会随着该代码被load到不同的地址而有不同的值,这就导致了代码段在load到不同地址时,内容(第一句mov指令)会不同,这就导致了无法share。

		.text
		movl	$one_byte, %ebx
		movl	(%ebx), %eax
		.data
		.align	4
	one_dword:
		.byte	1

以ELF格式为例来说明PIC如何避免地址修正。在ELF格式中,各个代码段数据段都有固定的位置,代码段中某条代码的位置到数据段的地址都是固定的。所以如果某条指令要引用一个数据的时候,如果能得到当前指令的地址,就可以通过加上到数据段的固定偏移来找到这个数据。ELF格式中引入了Global Offset Table (GOT)来实现这个机制。GOT就是一系列地址的数组,包含了所有全局数据的地址。

         
			|		|
			|---------------|  
			|  data_A	|
	Data section	|---------------| 
			|  data_B	|
		------->|---------------| 
			|		|
			|      ...	|
			|		|
			|---------------| 
			| addr of data_A|
			|---------------|  
		GOT base| addr of data_B|
		------->|---------------| 
			|		|
			|		|
			|		|
			|      ...	|
	Text section	|		|
		------->|---------------| 


根据GOT,经过下面的三步,就可以寻址到特定数据。
    1)得到当前指令的地址
    2)根据固定的偏移找到GOT的基地址
    3)根据数据的符号固定偏移找到该数据的地址

下面的代码实现了这一过程:

   

		call 	tmp_label						/* will push EIP to stack */
	tmp_label:	
		popl	%ecx							/* %ecx now has address of $tmp_label */
		addl	$GOT_TABLE_OFFSET_TO_CUR +[. - $tmp_label], %ecx	/* %ecx now has the base address of GOT */



上面的代码中,GOT_TABLE_OFFSET_TO_CUR是所在代码的地址到GOT基地址的固定偏移,这个是compiler & linker决定的。执行上面最后一句后,%ecx里已经是GOT的基地址了,然后就可以根据%ecx寻址数据:

		movl	data_symbol_offset(%ecx), %ebx	/* %ebx now has address of target data */
		movl	(%ebx), %eax			/* move data to %eax */

data_symbol_offset也是在编译连接的过程中确定,是固定值。

一个library中包含了一个全局的GOT,每个library都有自己的GOT。在load时,GOT对每个进程都是私有的,这个和数据段一样。如果某个library引用了另一个library的数据,则该library的GOT里也包含这个数据的地址,只不过这个地址是在dynamic linker在load library的时候负责填入的,编译链接阶段无法确定这个值,ELF格式中定义了特定的类型来表示这种数据。

3)一个具体例子

写一个简单的例子来验证PIC的实现。libtest2.so只包含一个数据,被libtest.so引用,main调用libtest.so里的test函数。

kai@opensolaris-kai:~/src/tmp$ cat test.c
static int data = 1;
extern int test2_data;

void test(int p1, int p2, int p3)
{
	data += p2;
	test2_data += p1;
}

kai@opensolaris-kai:~/src/tmp$ cat test2.c
int test2_data = 3;

kai@opensolaris-kai:~/src/tmp$ cat main.c 
void test(int p1, int p2, int p3);

int main(void)
{
	int i = 5;
	
	test(2, i, 2);
}

kai@opensolaris-kai:~/src/tmp$ make
gcc -nostdlib -shared -fPIC -s -o libtest2.so test2.c
gcc -nostdlib -shared -fPIC -s -o libtest.so test.c -ltest2
gcc -o main main.c -ltest
objdump -D libtest2.so > test2.S
objdump -D libtest.so > test.S
objdump -D main > main.S


反汇编后的test.S:

Disassembly of section .text:

0000056c <test>:
 56c:   55                      push   %ebp			/* %ebp指向caller的stack frame base pointer */
 56d:   89 e5                   mov    %esp,%ebp		/* %esp指向test的stack frame base pointer,存进%ebp,用来访问传给test的参数 */
 56f:   53                      push   %ebx			
 570:   e8 00 00 00 00          call   575 <test+0x9>		/* 地址575会被压入stack */
 575:   5b                      pop    %ebx			/* %ebx现在等于地址575 */
 576:   81 c3 2b 00 01 00       add    $0x1002b,%ebx		/* %ebx现在等于GOT的base address,0x1002b是compiler & linker计算的 */
 57c:   8b 45 0c                mov    0xc(%ebp),%eax		/* 把p2的值move到%eax,传给test的三个参数分别在%ebp+0x8, %ebp + 0xc, %ebp + 0x10 */
 57f:   01 83 10 00 00 00       add    %eax,0x10(%ebx)		/* %ebp+0x10直接指向了libtest.so里的data,这里没有经过GOT去寻址,猜测应该是经过优化了 */
 585:   8b 8b 0c 00 00 00       mov    0xc(%ebx),%ecx		/* %ecx, %edx都指向libtest2.so里的test2_data */	
 58b:   8b 93 0c 00 00 00       mov    0xc(%ebx),%edx
 591:   8b 45 08                mov    0x8(%ebp),%eax		/* move test2_data到%eax */
 594:   03 02                   add    (%edx),%eax		/* add p1 to %eax */
 596:   89 01                   mov    %eax,(%ecx)		/* store %eax back to test2_data */
 598:   5b                      pop    %ebx
 599:   83 c4 00                add    $0x0,%esp		/* ? */
 59c:   c9                      leave
 59d:   c3                      ret

Disassembly of section .got:

000105a0 <_GLOBAL_OFFSET_TABLE_>:
   105a0:       94                      xchg   %eax,%esp
        ...

Disassembly of section .data:

000105b0 <_edata-0x4>:
   105b0:       01 00                   add    %eax,(%eax)
        ...


一个问题:对于library自身数据的访问,似乎不需要GOT?因为可以数据段到代码的偏移也是固定的,完全可以直接得到数据段基地址。

给出执行了push %ebx后的stack的情况(期间有一次地址575的push和pop)。
SFBP = stack frame base pointer
            
					|			|  
	stack top when enter main  --->	|-----------------------| 
					| 			|  
					|			|  					
					|			|  
					| 			|
					|-----------------------| 
					|  	p3		|
					|-----------------------| 
					|	p2		|
					|-----------------------|
					|	p1		|
					|-----------------------| 
					| return address in main|
	stack top when enter test  --->	|-----------------------|  
					| SFBP of main  	| 
					|-----------------------|  <--- EBP 
					| original %ebx		|
					|-----------------------|  <--- ESP
					|			|
					|      			|
           

4) Advantage & disadvantage of PIC

PIC的好处显然是在load时可以被load到任意位置而不需要代码段的地址修正,代码可以被不同process share而只留有一份代码在内存中。坏处是增加了额外的对GOT的引用,以及一系列必须的额外的开销(比如对代码段地址的call 和pop等),使得代码运行速度比飞PIC的慢。
另外,由于GOT里的数据地址也是需要在load时计算的,所以对于一些拥有大量数据的library,load的时间也会变慢。

一个问题:对于library自身数据的访问,似乎不需要GOT?因为可以数据段到代码的偏移也是固定的,完全可以直接得到数据段基地址。这似乎可以大大减少load时对GOT里地址的修正所带来的额外时间的花销。

5) Reference

a. Intel IA-32 Architectures Manual Volume1 Basic Architecture, CHAPTER 6, PROCEDURE CALLS, INTERRUPTS, AND EXCEPTIONS
b. Linkers & Loaders, Chapter 8, Loading and overlays, Position indenpendent code
c. http://bottomupcs.sourceforge.net/csbu/x3824.htm            
                



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值