Tinkering with Linux Execution Environment(3)

elf3

linux_logo_08

Yet again we are at the point of pursuing the previous part of ELF explanation. In this part we focus more on dynamic variables.

As I said before, the detailed procedure of loading and executing a file in OS is much more sophisticated than it can be explained in some short blog posts. So let’s keep the discussion as useful and concise as possible. Before proceeding to read this post, take a look at my previous PLT article (If you already haven’t). Here I used similar code to what examined in the aforementioned post with little modifications. Below I’ve written the code again:

libtest.h:
#include 
extern int foo();
extern int dyn_var;
extern int data_var;
-------------------------------------------
libtest.c:
#include "libtest.h"
int data_var=4;
int dyn_var;
int foo(){
    dyn_var=2;
    data_var=7;
    printf("Regular function in library\n");
    return 0;
}
-------------------------------------------
driver.c:
#include "libtest.h"
#include 
int local_var;
int main(){
    local_var = 3;
    dyn_var = 1;
    data_var=6;
    printf("dyn var/data var before call to lib: %d %d \n",dyn_var,data_var);
    foo();   
    printf("dyn var/data var after call to lib: %d %d\n",dyn_var,data_var);
    return 0;
}

As you see in the lib code I added some more variables. dyn_var is not initialized and should be stored in dynamic object’s .bss segment. The other variable data_var is initialized in library and should be resident in .data segment of the dynamic object. Now let’s take a look at the driver’s ELF specification:

table '.dynsym' :
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     4: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND foo
     6: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
     7: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
     8: 0000000000600bf8     0 NOTYPE  GLOBAL DEFAULT   24 _edata
     9: 0000000000600bf8     4 OBJECT  GLOBAL DEFAULT   25 data_var
    10: 0000000000600c08     0 NOTYPE  GLOBAL DEFAULT   25 _end
    11: 0000000000600bf8     0 NOTYPE  GLOBAL DEFAULT   25 __bss_start
    12: 0000000000600bfc     4 OBJECT  GLOBAL DEFAULT   25 dyn_var
    13: 00000000004005c0     0 FUNC    GLOBAL DEFAULT   11 _init
    14: 0000000000400814     0 FUNC    GLOBAL DEFAULT   14 _fini

Name[hex]			data_var
Symbole scope:			Global
Symbole type:			Data object
Defined in section		25 [.bss]
Value			        0x0000000000600BD8
Symbole size			4

Name[hex]			dyn_var
Symbole scope:			Global
Symbole type:			Data object
Defined in section		25 [.bss]
Value			        0x0000000000600BDC
Symbole size			4

Both variables are defined in .bss sections. The corresponding entry for dynamic object is:

Name[hex]			data_var
Symbole scope:			Global
Symbole type:			Data object
Defined in section		22 [.data]
Value			        0x0000000000200A58
Symbole size			4

Name[hex]			dyn_var
Symbole scope:			Global
Symbole type:			Data object
Defined in section		23 [.bss]
Value			        0x0000000000200A60
Symbole size			4

The variable is in .data segment. Remember this variable is initialized in object file. Well, here’s the disassembly of the interesting functions:

Dump of assembler code for function main:
   0x0000000000400726 <+0>:	push   rbp
   0x0000000000400727 <+1>:	mov    rbp,rsp
   0x000000000040072a <+4>:	mov    DWORD PTR [rip+0x2004b0],0x3        # 0x600be4 
   0x0000000000400734 <+14>:	mov    DWORD PTR [rip+0x20049e],0x1        # 0x600bdc 
   0x000000000040073e <+24>:	mov    DWORD PTR [rip+0x200490],0x6        # 0x600bd8 
   0x0000000000400748 <+34>:	mov    eax,DWORD PTR [rip+0x20048e]        # 0x600bdc 
   0x000000000040074e <+40>:	mov    esi,eax
   0x0000000000400750 <+42>:	mov    edi,0x400818
   0x0000000000400755 <+47>:	mov    eax,0x0
   0x000000000040075a <+52>:	call   0x4005f0 <printf@plt>
   0x000000000040075f <+57>:	mov    eax,0x0
   0x0000000000400764 <+62>:	call   0x400620 <foo@plt>
   0x0000000000400769 <+67>:	mov    eax,DWORD PTR [rip+0x20046d]        # 0x600bdc 
   0x000000000040076f <+73>:	mov    esi,eax
   0x0000000000400771 <+75>:	mov    edi,0x400840
   0x0000000000400776 <+80>:	mov    eax,0x0
   0x000000000040077b <+85>:	call   0x4005f0 <printf@plt>
   0x0000000000400780 <+90>:	mov    eax,0x0
   0x0000000000400785 <+95>:	pop    rbp
   0x0000000000400786 <+96>:	ret    

Dump of assembler code for function foo:
   0x00007ffff7bdb720 <+0>:	push   rbp
   0x00007ffff7bdb721 <+1>:	mov    rbp,rsp
   0x00007ffff7bdb724 <+4>:	mov    rax,QWORD PTR [rip+0x2002e5]        # 0x7ffff7ddba10
   0x00007ffff7bdb72b <+11>:	mov    DWORD PTR [rax],0x2
   0x00007ffff7bdb731 <+17>:	mov    rax,QWORD PTR [rip+0x2002b8]        # 0x7ffff7ddb9f0
   0x00007ffff7bdb738 <+24>:	mov    DWORD PTR [rax],0x7
   0x00007ffff7bdb73e <+30>:	lea    rdi,[rip+0x18]        # 0x7ffff7bdb75d
   0x00007ffff7bdb745 <+37>:	call   0x7ffff7bdb5f0 <puts@plt>
   0x00007ffff7bdb74a <+42>:	mov    eax,0x0
   0x00007ffff7bdb74f <+47>:	pop    rbp
   0x00007ffff7bdb750 <+48>:	ret    

Access to dyn_var and data_var in the main function which is part of the driver is done through addresses 0x600bdc and 0x600bd8 respectively. Also we have a local variable which is similarly defined in .bss of the main section.
In function foo() at +4 and +18. The addresses are stored in 0x7ffff7ddba10 and 0x7ffff7ddb9f0. These two last addresses are dereferenced during execution to retrieve the addresses to dyn_var and data_var:

(gdb) x/xg 0x7ffff7ddba10
0x7ffff7ddba10:	0x0000000000600bfc
(gdb) x/xg 0x7ffff7ddb9f0
0x7ffff7ddb9f0:	0x0000000000600bf8

The stored addresses are what the driver uses to access dyn_var and data_var in its .bss section.

So we have one copy of these variables (as expected) both stored in bss of the driver. We run the program to see the output:

$ ./test
dyn var/data var before call to lib: 1 6 
Regular function in library
dyn var/data var after call to lib: 2 7

Both objects are using same variables which are defined in shared object and reside in one .bss section. As you can infer from the output, they are pointing to same addresses for same variables. The address to dyn_var and data_var are stored in .got section of dynamic object.

Name[hex]			[Empty]
Symbole scope:			Local
Symbole type:			Symbol associated with a section
Defined in section			20 [.got]
Value			0x00000000002009E8
Symbole size				0

The offset of .got section is 2009E8. The entry point of shared object in memory is 7ffff7bdb000. So the got starts at 7ffff7ddb9E8. The references to dyn_var and data_var are after this address and very near to it. So they are in .got table. In .got segment of dynamic object correct addresses to our variables are stored. By accessing the got, the code of shared object doesn’t need to know the absolute addresses. By comparing the code in main function of driver and foo in the shared object you can discern that the the relative addresses in the former function are for directly calculating the absolute addresses of the two variables but the addresses in foo function of the shared object are pointers to the got table and this is one of the facilities to make having P.I.C programs possible cause the code of the shared object need not be changed before being loaded into different binaries.

Contact: sirus.shahini@gmail.com
         twitter.com/_BitWar
Iran University of Science and Technology
Department of Computer Engineering

Leave a Reply

Your email address will not be published. Required fields are marked *