天天看点

MIPS PIC概述 --有关 .MIPS.stubs

http://vm-kernel.org/cnblog/2010/04/mips-pic%E6%A6%82%E8%BF%B0/

MIPS PIC概述

PIC是一个很重要的概念,它是position independent code的简称.它的意思表明这些code可以被load到任何地方去执行. 这样做的好处在于对于共享库,我们可以在内存中只保留一份,然后所有其他的程序都只调用这一份共享库中的代码. 因此,需要共享库的代码是不依赖与其在内存中的地址的.

为了能实现这个目标,我们需要一个成为GOT(global offset table)的东西. 在MIPS中, 这个表的地址保存在gp寄存器中.在其他架构中,会有其他的实现方法.具体可以查阅ABI手册.这个表中存放全局的数据和要用到的外部函数(比如 printf)的地址.然后在调用这些函数的时候,会先从GOT表中取出表项,然后再跳转.汇编语句如下:

40065c:    8f998038     lw    t9,-32712(gp)  ->取出GOT表项,值是要调用的函数地址

400660:    00000000     nop

400664:    0320f809     jalr    t9  ->跳转到这个函数

400668:    00000000     nop

当 我们调用printf函数的时候,由于printf函数位于libc中,因此在函数被link的时候并不知道prinf函数位于什么地方.这个时候,需要 在GOT表中关于printf的部分填入一个stub值. 那么这个stub值什么时候被修改成printf函数真正的地址呢? 有两个方法.

(1) 方法1. 当程序被加载执行的时候,parse GOT表,找出类似于printf这种还不知道函数具体地址的表项.然后去libc或者其他lib中查找这些函数在内存中地址.当然如果这些lib还没有 被加载到内存,必须首先把库加载到内存. 这样由于所有必需的库都已经在内存中,他们的地址也就确定下来.因此,这个时候可以修改本程序的GOT表,把这些函数在内存中的地址写到GOT表项中.这 样就可以通过GOT表调用到printf这些函数.

这个方面咋听上去不错.但是有一个致命的问题是什么呢?由于在程序加载的时候,需 要parse所有外部函数的地址.如果这些函数很多,那么将会花很长很长的时间,其间可能经历了无数次的cache miss等等. 而且有一些函数虽然在程序中被调用了,但是这些函数在实际运行的时候并不会被跑到(比如在一个判断语句后被调用的函数,而这个判断语句的结果在运行的时候 不满足).因此,这个对于性能来说是一个很大的损失. 也许你调用一个程序后,出去倒杯水回来,程序还没有加载完呢(当然,太极端的例子). 因此,现在实际的情况并不是采用这种方法,而是采用方法2.

(2) 方法2. 这种方法被称为fully dynamic linking. 也就是说,把重填GOT表项的时间从加载时推迟到调用的时候.也就是说,只有这个外部函数被调用的时候,才会去重填这个表项,否则不去做. 这种方法有点类似于把事情推到非做不可的时候再做的意思,因此也被称为lazy linking. 为了实现lazy linking,除了GOT表外, MIPS使用了称为.MIPS.stubs的section. 在GOT表中外部函数的表项被指向了.MIPS.stubs. 当程序运行时,如果是第一次调用printf等外部函数,会跑到这个section中. 然后这个section会调用动态连接器来得到printf的真正地址,并且更新GOT表项. 下一次再调用这个函数的时候,就可以直接调用这些外部函数. 过程如下图:

MIPS PIC概述 --有关 .MIPS.stubs

第一次调用printf

MIPS PIC概述 --有关 .MIPS.stubs

第二次调用printf

另 外还有一个问题,对于PIC code, 它的GOT表在内存中的位置也是不固定的.那么该如何得到GOT表的位置呢? 解决方法是虽然GOT表的位置是不固定的,但是呢,GOT表和当前函数的偏移是固定的,并且当前函数的地址是放在寄存器t9中的. t9 + (GOT 和 当前函数的偏移) 不就得到GOT表的地址了吗. 请记住,这是有前提的.前提是 进入PIC code 的某一个函数中, t9必需等于当前函数在内存中的地址.这个有函数的调用者(caller)保证. 另外,在被调用函数(callee)部分,需要先保存gp的值,然后在函数返回的时候恢复gp的值.

因此,可以通过下面汇编代码来得到当前的GOT表:

400640:    3c1c0002     lui    gp,0x2

400644:    279c82f0     addiu    gp,gp,-32016   ->gp = got - 当前函数地址

400648:    0399e021     addu    gp,gp,t9         -> gp + t9 = got

另外,对于动态库函数,必需是PIC,而对于一般的应用程序来说,不需要是PIC的.并且在MIPS中,默认的应用也不是PIC的.如果需要使得生成的应用是PIC的,必须在编译的时候叫上 -mshared 编译参数.

下 面使用一个例子来说明MIPS 动态连接的过程. 这个例子运行的环境是 debian lenny. CPU是loongson 2f. gcc的版本是gcc version 4.3.2 (Debian 4.3.2-1.1). gdb的版本是GNU gdb 6.8-debian.

源文件:

main-pic.c test-pic.c

反汇编文件:

main-pic.o.asm test-pic.o.asm test-pic.asm

编译命令:

gcc -c -mshared -g main-pic.c

gcc -c -mshared -g test-pic.c

gcc -o   test-pic main-pic.o test-pic.o

objdump -d main-pic.o > main-pic.o.asm

objdump -d test-pic.o > test-pic.o.asm

objdump -d test-pic > test-pic.asm

[email protected]:~/dev$ gdb test

GNU gdb 6.8-debian

Copyright (C) 2008 Free Software Foundation, Inc.

License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software: you are free to change and redistribute it.

There is NO WARRANTY, to the extent permitted by law.  Type "show copying"

and "show warranty" for details.

This GDB was configured as "mipsel-linux-gnu"...

(gdb) b main

Breakpoint 1 at 0x400660: file main-pic.c, line 7.

(gdb) r

Starting program: /home/yajin/dev/test-pic

Breakpoint 1, main () at main-pic.c:7

7        test();

(gdb) p /x $pc

$1 = 0x400660

(gdb) p /x $gp

$2 = 0x418930    ->gp=GOT的位置0x418930

(gdb) p /x $t9

$3 = 0x400640   ->t9=当前函数地址

(gdb) si

0x00400664    7        test();

(gdb) p /x $t9   ->t9=要调用的函数test的地址

$4 = 0x400690

(gdb) si

0x00400668    7        test();

(gdb) si

test () at test.c:4

4    {

(gdb) p /x $pc

$5 = 0x400690

(gdb) si

0x00400694    4    {

(gdb) si

0x00400698    4    {

(gdb) si

0x0040069c    4    {

(gdb) p /x $gp

$6 = 0x418930   ->发现没有?gp=0x418930,是GOT的位置

(gdb) p /x $pc

$7 = 0x40069c

(gdb) si

0x004006a0    4    {

(gdb)

0x004006a4    4    {

(gdb)

0x004006a8    4    {

(gdb)

0x004006ac    4    {

(gdb)

5        printf("hello world first /n");

(gdb)

0x004006b4    5        printf("hello world first /n");

(gdb)

0x004006b8    5        printf("hello world first /n");

(gdb) p /x $gp

$8 = 0x418930

(gdb) si

0x004006bc    5        printf("hello world first /n");

(gdb) p /x $gp

$9 = 0x418930

(gdb) p /x $pc

$10 = 0x4006bc

(gdb) si

0x004006c0    5        printf("hello world first /n");

(gdb) p /x $t9

$11 = 0x400840   ->这里是要跳转到的.MIPS.stubs的位置

(gdb) si

0x004006c4    5        printf("hello world first /n");

(gdb) si ->跳转到.MIPS.stubs

0x00400840 in puts ()

Current language:  auto; currently asm

(gdb) si

0x00400844 in puts ()

(gdb) p /x $pc

$12 = 0x400844

(gdb) p /x $t9

$13 = 0x2aabf4d0

(gdb) si

0x00400848 in puts ()

(gdb) si   ->跳转到动态连接器中

0x2aabf4d0 in _dl_runtime_resolve () from /lib/ld.so.1

(gdb) b *0x4006cc

Breakpoint 2 at 0x4006cc: file test.c, line 5.

(gdb) c

Continuing.

hello world first

Breakpoint 2, 0x004006cc in test () at test-pic.c:5

5        printf("hello world first /n");

Current language:  auto; currently c

(gdb) p /x $gp

$14 = 0x2ac55950

(gdb) p /x $pc

$15 = 0x4006cc

(gdb) si

0x004006d0    5        printf("hello world first /n");

(gdb) p /x $gp

$16 = 0x418930   ->第一次printf结束,gp恢复过来了

(gdb) si

6        printf("hello world second /n");

(gdb) p /x $pc

$17 = 0x4006d4

(gdb) si

0x004006d8    6        printf("hello world second /n");

(gdb)

0x004006dc    6        printf("hello world second /n");

(gdb)

0x004006e0    6        printf("hello world second /n");

(gdb) si

0x004006e4    6        printf("hello world second /n");

(gdb) p /x $t9  ->第二次直接到printf,不会到.MIPS.Stubs

$18 = 0x2ab444d0

(gdb) p /x $pc

$19 = 0x4006e4

(gdb) si

0x004006e8    6        printf("hello world second /n");

(gdb) p /x $pc

$20 = 0x4006e8

(gdb) si

本文中的例子是采用c来写的.关于相关的汇编,请参考下面的连接. 另外还有一些宏 _gp_disp,__gnu_local_gp,cpload $25 等,理解了本文就不难理解了.

参考:

[1] Some new¹ tricks for better performance in MIPS-Linux

[2] Position-Independent Coding in Assembly Language

[3] PIC CODE

标签: loongson, MIPS, PIC