gcc编译参数-fPIC的一些问题

2019-04-15 13:21发布


转自:http://blog.sina.com.cn/s/blog_54f82cc201011op1.html
gcc编译参数-fPIC的一些问题 (2012-07-26 15:41:08)转载▼
标签: linux compiler gcc -fpic it 分类: NSN_BspDriver
ppc_85xx-gcc -shared -fPIC liberr.c -o liberr.so

-fPIC 作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code),
  则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意
  位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。

gcc -shared -fPIC -o 1.so 1.c
这里有一个-fPIC参数
PIC就是position independent code
PIC使.so文件的代码段变为真正意义上的共享
如果不加-fPIC,则加载.so文件的代码段时,代码段引用的数据对象需要重定位, 重定位会修改代码段的内容,这就造成每个使用这个.so文件代码段的进程在内核里都会生成这个.so文件代码段的copy.每个copy都不一样,取决于 这个.so文件代码段和数据段内存映射的位置.

不加fPIC编译出来的so,是要再加载时根据加载到的位置再次重定位的.(因为它里面的代码并不是位置无关代码)
如果被多个应用程序共同使用,那么它们必须每个程序维护一份so的代码副本了.(因为so被每个程序加载的位置都不同,显然这些重定位后的代码也不同,当然不能共享)
我们总是用fPIC来生成so,也从来不用fPIC来生成a.
fPIC与动态链接可以说基本没有关系,libc.so一样可以不用fPIC编译,只是这样的so必须要在加载到用户程序的地址空间时重定向所有表目.

因此,不用fPIC编译so并不总是不好.
如果你满足以下4个需求/条件:
1.该库可能需要经常更新
2.该库需要非常高的效率(尤其是有很多全局量的使用时)
3.该库并不很大.
4.该库基本不需要被多个应用程序共享

如果用没有加这个参数的编译后的共享库,也可以使用的话,可能是两个原因:
1:gcc默认开启-fPIC选项
2:loader使你的代码位置无关

从GCC来看,shared应该是包含fPIC选项的,但似乎不是所以系统都支持,所以最好显式加上fPIC选项。参见如下

`-shared'
     Produce a shared object which can then be linked with other
     objects to form an executable.  Not all systems support this
     option.  For predictable results, you must also specify the same
     set of options that were used to generate code (`-fpic', `-fPIC',
     or model suboptions) when you specify this option.(1)

-fPIC 的使用,会生成 PIC 代码,.so 要求为 PIC,以达到动态链接的目的,否则,无法实现动态链接。

non-PIC 与 PIC 代码的区别主要在于 access global data, jump label 的不同。
比如一条 access global data 的指令,
non-PIC 的形势是:ld r3, var1
PIC 的形式则是:ld r3, var1-offset@GOT,意思是从 GOT 表的 index 为 var1-offset 的地方处
指示的地址处装载一个值,即var1-offset@GOT处的4个 byte 其实就是 var1 的地址。这个地址只有在运行的时候才知道,是由 dynamic-loader(ld-linux.so) 填进去的。

再比如 jump label 指令
non-PIC 的形势是:jump printf ,意思是调用 printf。
PIC 的形式则是:jump printf-offset@GOT,
意思是跳到 GOT 表的 index 为 printf-offset 的地方处指示的地址去执行,
这个地址处的代码摆放在 .plt section,
每个外部函数对应一段这样的代码,其功能是呼叫dynamic-loader(ld-linux.so) 来查找函数的地址(本例中是 printf),然后将其地址写到 GOT 表的 index 为 printf-offset 的地方,
同时执行这个函数。这样,第2次呼叫 printf 的时候,就会直接跳到 printf 的地址,而不必再查找了。

GOT 是 data section, 是一个 table, 除专用的几个 entry,每个 entry 的内容可以再执行的时候修改;
PLT 是 text section, 是一段一段的 code,执行中不需要修改。
每个 target 实现 PIC 的机制不同,但大同小异。比如 MIPS 没有 .plt, 而是叫 .stub,功能和 .plt 一样。

可见,动态链接执行很复杂,比静态链接执行时间长;但是,极大的节省了 size,PIC 和动态链接技术是计算机发展史上非常重要的一个里程碑。

gcc manul上面有说
-fpic        If the GOT size for the linked executable exceeds a machine-specific maximum size, you get an error message from the linker indicating that -fpic does not work; in that case, recompile with -fPIC instead. (These maximums are 8k on the SPARC and 32k on the m68k and RS/6000. The 386 has no such limit.)

-fPIC       If supported for the target machine, emit position-independent code, suitable for dynamic linking and avoiding any limit on the size of the global offset table. This option makes a difference on the m68k, PowerPC and SPARC. Position-independent code requires special support, and therefore works only on certain machines.

关键在于GOT全局偏移量表里面的跳转项大小。
intel处理器应该是统一4字节,没有问题。
powerpc上由于汇编码或者机器码的特殊要求,所以跳转项分为短、长两种。

-fpic为了节约内存,在GOT里面预留了“短”长度。
而-fPIC则采用了更大的跳转项。

//=============================================================================
另外一个参考:http://lcinx.blog.163.com/blog/static/43494267201121211316355/ 2011-03-12 23:31:06|  分类: C/C++开发 |举报|字号 订阅 若涉及so共享库文件。

则可能要在编译目标代码时指定编译参数 -fPIC   意为:编译为位置无关的目标文件。

注: 至于为啥32位下的共享库没指定还可以?  迷惑。  若有知道原因的朋友,可以告知下吗? 不胜感激!

想起来搜下fPIC 与 shared,找出来这么一篇文章,怕文章以后找不到,直接复制过来了。

地址为:这里

编译GNU/Linux共享库, 为什么要用PIC编译? 

       一直以为不管是编译共享库还是静态库,中间生成的目标文件(.o文件)是没有区别的,区别只在:最后是用-shared编译还是用ar打包; 可是事情的真相并不是这样的:
from 《Binary Hacks:黑客秘笈100选》

本hack中,我们来研究编译共享库时,为什么要用PIC(选项)编译?

通常编译GNU/Linux共享库时,把各个.c文件编译编译成PIC(Position Independent Code, 位置无关代码)。但是,实际上不用PIC编译的话也可以编译共享库。那么使用PIC还有意义吗?

让我们来进行一个实验:
    #include
    void func() {
        printf(" ");
        printf(" ");
        printf(" ");
    }

用 PIC编译必须把参数-fpic或-fPIC传给gcc,-fpic可以生成小而高效的代码,但是不同的处理器中-fpic生成的GOT(Global Offset Table, 全局偏移表)的大小有限制,另一方面,使用-fPIC的话,任何处理器都可以放心使用。在这里,使用-fPIC。(在X86中使用-fpic和-fPIC 没有任何区别)。
    $ gcc -o fpic-no-pic.s -S fpic.c
    $ gcc -fPIC -o fpic-pic.s -S fpic.c

阅读上述生成的汇编代码,则可以知道PIC版本通过PLT(Procedure Linkage Table)调用printf。
    $ grep printf fpic-no-pic.s
             call printf
             call printf
             call printf
    $ grep printf fpic-pic.s
             call printf@PLT
             call printf@PLT
             call printf@PLT
下面,编译共享库
    $ gcc -shared -o fpic-no-pic.so fpic.c
    $ gcc -shared -fPIC -o fpic-pic.so fpic.c

这 些共享库的动态节(dynamic section)用readelf阅读的话,非PIC版本中有TEXTREL输入方法(需要在text内进行再配置),并且RELCOUNT(再配置的数 量)为5 -- 比PIC版本的多3个。多出三个是因为printf()的调用进行了3次。
    $ readelf -d fpic-no-pic.so | egrep 'TEXTREL|RELCOUNT'
     0x00000016 (TEXTREL)                  0x0
     0x6ffffffa (RELCOUNT)                 5
    $ readelf -d fpic-pic.so | egrep 'TEXTREL|RELCOUNT'
     0x6ffffffa (RELCOUNT)  2

PIC版本的RELCOUNT非0是由于gcc在缺省时使用的是包含在启动文件里的代码。若加-nostartfiles选项,则RELCOUNT值为0。

PIC和非PIC共享库的性能对比
上面例子阐述了非PIC版本运行时(动态运行时)需要5个地址的再分配。那么,若在配置的数量大增时会出现什么样的情况呢?

运行下面的shell脚本,用非PIC版本和PIC版本编译含有1000万次printf()调用的共享库,和相应的可执行文件fpic-no-pic和fpic-pic。

    #! /bin/sh
    rm -f *.o *.so
    num=1000
    for i in `seq $num`; do
        echo -e "#include void func$i() {" >fpic$i.c
        #ruby -e "10000.times { puts 'printf(" ");' }" >>fpic$i.c
        perl -e 'print("printf(" "); "x10000);' >>fpic$i.c
        echo "}" >> fpic$i.c
        gcc -o fpic-no-pic$i.o -c fpic$i.c
        gcc -o fpic-pic$i.o -fPIC -c fpic$i.c
    done
    gcc -o fpic-no-pic.so -shared fpic-no-pic*.o
    gcc -o fpic-pic.so -shared fpic-pic*.o

    echo "int main() { return 0; }" >fpic-main.c
    gcc -o no-pic-load fpic-main.c ./fpic-no-pic.so
    gcc -o pic-load fpic-main.c ./fpic-pic.so

    echo "int main() {" >main.c
    for i in `seq $num`; do echo "func$i();"; done >>main.c
    echo "}" >>main.c
    gcc -o fpic-no-pic main.c ./fpic-no-pic.so
    gcc -o fpic-pic main.c ./fpic-pic.so

两个版本程序,运行结果如下: 非PIC版本首次运行时间2.15秒,第二次以后大约0.55秒,而PIC版本的首次用了0.02秒,第二次以后用了0.00秒。

    $ repeat 3 time ./no-pic-load
    2.15s total : 0.29s user 0.48s system 35% cpu
    0.56s total : 0.25s user 0.31s system 99% cpu
    0.55s total : 0.30s user 0.25s system 99% cpu
    $ repeat 3 time ./pic-load
    0.02s total : 0.00s user 0.00s system 0% cpu
    0.00s total : 0.00s user 0.01s system 317% cpu
    0.00s total : 0.00s user 0.00s system 0% cpu
main() 本身是空的,可以知道非PIC版本动态链接时的再分配需要2.15~0.55秒。运行环境Xeon-2.8GHz+Debian GNU/Linux sarge+GCC 3.3.5。

非PIC版本的缺点不仅是在运行时再配置上花时间。为了更新再配置部分的代码,将会发生这样的情况(下载text segment里需要再配置的页->更新->进行copy on write -> 不能与其他路径和text共享)。

另外比较非PIC版本的fpic-no-pic.so和PIC版本的fpic-pic.so的大小,前者268M,后者134M,差别很明显。用readelf -S查看节头,会有以下区别:
                 .rel.dyn      .text
非PIC            152MB         114MB
PIC              0MB           133MB

非PIC版本的代码(.text)比PIC版本的小,但再配置所需要的信息占很大的空间。