Please indicate the source: http://blog.csdn.net/gaoxiangnumber1.
7.8 Executable Object Files
The ELF header describes the overall format of the file. It includes the program’s entry point, which is the address of the first instruction to execute when the program runs.
The .text, .rodata, and .data sections are similar to those in a relocatable object file, except that these sections have been relocated to their eventual run-time memory addresses.
The .init section defines a small function, called _init, that will be called by the program’s initialization code.
Since the executable is fully linked (relocated), it needs no .rel sections.
ELF executables are designed to be easy to load into memory, with contiguous chunks of the executable file mapped to contiguous memory segments. This mapping is described by the segment header table.
From the segment header table, we see that two memory segments will be initialized with the contents of the executable object file.
Lines 1 and 2 tell that the first segment (the code segment) is aligned to a 4 KB (212) boundary, has read/execute permissions, starts at memory address 0x08048000, has a total memory size of 0x448 bytes, and is initialized with the first 0x448 bytes of the executable object file, which includes the ELF header, the segment header table, and the .init, .text, and .rodata sections.
Lines 3 and 4 tell us that the second segment (the data segment) is aligned to a 4 KB boundary, has read/write permissions, starts at memory address 0x08049448, has a total memory size of 0x104 bytes, and is initialized with the 0xe8 bytes starting at file offset 0x448, which in this case is the beginning of the .data section. The remaining bytes in the segment correspond to .bss data that will be initialized to zero at run time.
7.9 Loading Executable Object Files
To run an executable object file p, we can type its name to the Unix shell’s command line:
unix> ./p
Since p does not correspond to a built-in shell command, the shell assumes that p is an executable object file, which it runs for us by invoking some memory-resident operating system code known as the loader. Any Unix program can invoke the loader by calling the execve function. The loader copies the code and data in the executable object file from disk into memory, and then runs the program by jumping to its first instruction, or entry point. This process of copying the program into memory and then running it is known as loading.
On 32-bit Linux systems
The code segment starts at address 0x08048000.
The data segment follows at the next 4 KB aligned address.
The run-time heap follows on the first 4 KB aligned address past the read/write segment and grows up via calls to the malloc library.
There is also a segment that is reserved for shared libraries.
The user stack always starts at the largest legal user address and grows down (toward lower memory addresses).
The segment starting above the stack is reserved for the code and data in the memory-resident part of the operating system known as the kernel.
When the loader runs, it creates the memory image shown in Figure 7.13. Guided by the segment header table in the executable, it copies chunks of the executable into the code and data segments. Next, the loader jumps to the program’s entry point, which is always the address of the _start symbol. The startup code at the _start address is defined in the object file crt1.o and is the same for all C programs.
Figure 7.14 shows the specific sequence of calls in the startup code. After calling initialization routines from the .text and .init sections, the startup code calls the atexit routine, which appends a list of routines that should be called when the application terminates normally. The exit function runs the functions registered by atexit, and then returns control to the operating system by calling _exit. Next, the startup code calls the application’s main routine, which begins executing our C code. After the application returns, the startup code calls the _exit routine, which returns control to the operating system.
How loading really works:
Each program in a Unix system runs in the context of a process with its own virtual address space. When the shell runs a program, the parent shell process forks a child process that is a duplicate of the parent. The child process invokes the loader via the execve system call. The loader deletes the child’s existing virtual memory segments, and creates a new set of code, data, heap, and stack segments. The new stack and heap segments are initialized to zero. The new code and data segments are initialized to the contents of the executable file by mapping pages in the virtual address space to page-sized chunks of the executable file. Finally, the loader jumps to the _start address, which eventually calls the application’s main routine. Aside from some header information, there is no copying of data from disk to memory during loading. The copying is deferred until the CPU references a mapped virtual page, at which point the operating system automatically transfers the page from disk to memory using its paging mechanism.
7.10 Dynamic Linking with Shared Libraries
Static libraries’ disadvantages.
Static libraries need to be maintained and updated periodically. If application programmers want to use the most recent version of a library, they must somehow become aware that the library has changed, and then explicitly relink their programs against the updated library.
Almost every C program uses standard I/O functions such as printf and scanf. At run time, the code for these functions is duplicated in the text segment of each running process. On a typical system that is running 50–100 processes, this can be a significant waste of scarce memory system resources.
A shared library is an object module that can be loaded at an arbitrary memory address and linked with a program in memory at run time. This process is known as dynamic linking and is performed by a program called a dynamic linker.
Shared libraries are referred to as shared objects, and on Unix systems are typically denoted by the .so suffix. Microsoft operating systems refer to as DLLs (dynamic link libraries).
Shared libraries are “shared” in two different ways.
First, in any given file system, there is exactly one .so file for a particular library. The code and data in this .so file are shared by all of the executable object files that reference the library, as opposed to the contents of static libraries, which are copied and embedded in the executables that reference them.
Second, a single copy of the .text section of a shared library in memory can be shared by different running processes.
Figure 7.15 summarizes the dynamic linking process for the example program in Figure 7.6.
To build a shared library libvector.so of routines in Figure 7.5:
unix> gcc -shared -fPIC -o libvector.so addvec.c multvec.c
The -fPIC flag directs the compiler to generate position-independent code. The -shared flag directs the linker to create a shared object file.
Once we have created the library, we would then link it into our example program in Figure 7.6:
unix> gcc -o p2 main2.c ./libvector.so
This creates an executable object file p2 in a form that can be linked with libvector.so at run time. The basic idea is to do some of the linking statically when the executable file is created, and then complete the linking process dynamically when the program is loaded.
None of the code or data sections from libvector.so are actually copied into the executable p2 at this point. Instead, the linker copies some relocation and symbol table information that will allow references to code and data in libvector.so to be resolved at run time.
When the loader loads and runs the executable p2, it loads the partially linked executable p2. Next, it notices that p2 contains a .interp section, which contains the path name of the dynamic linker, which is itself a shared object (e.g., ld-linux.so on Linux systems). Instead of passing control to the application, the loader loads and runs the dynamic linker.
The dynamic linker then finishes the linking task by performing the following relocations:
*Relocating the text and data of libc.so into some memory segment.
*Relocating the text and data of libvector.so into another memory segment.
*Relocating any references in p2 to symbols defined by libc.so and libvector.so.
Finally, the dynamic linker passes control to the application. From this point on, the locations of the shared libraries are fixed and do not change during execution of the program.
7.11 Loading and Linking Shared Libraries from Applications
We have discussed the scenario in which the dynamic linker loads and links shared libraries when an application is loaded, just before it executes. It is also possible for an application to request the dynamic linker to load and link arbitrary shared libraries while the application is running, without having to link those libraries at compile time.
7.12 Position-Independent Code (PIC)
A key purpose of shared libraries is to allow multiple running processes to share the same library code in memory and thus save precious memory resources. So how can multiple processes share a single copy of a program?
One approach would be to assign a priori(优先的) a dedicated chunk of the address space to each shared library, and then require the loader to always load the shared library at that address.
While straightforward, this approach creates some serious problems. It would be an inefficient use of the address space because portions of the space would be allocated even if a process didn’t use the library. Second, it would be difficult to manage. We would have to ensure that none of the chunks overlapped. Every time a library were modified, we would have to make sure that it still fit in its assigned chunk. If not, then we would have to find a new chunk. And if we created a new library, we would have to find room for it. Over time, given the hundreds of libraries and versions of libraries in a system, it would be difficult to keep the address space from fragmenting into lots of small unused but unusable holes. Even worse, the assignment of libraries to memory would be different for each system, thus creating even more management headaches.
A better approach is to compile library code so that it can be loaded and executed at any address without being modified by the linker. Such code is known as position-independent code (PIC). Users direct GNU compilation systems to generate PIC code with the -fPIC option to gcc.
On IA32 systems, calls to procedures in the same object module require no special treatment, since the references are PC-relative, with known offsets, and thus are already PIC. However, calls to externally defined procedures and references to global variables are not normally PIC, since they require relocation at link time.
PIC Data References
PIC Function Calls
7.13 Tools for Manipulating Object Files
Please indicate the source: http://blog.csdn.net/gaoxiangnumber1.
Chapter 7-03
最新推荐文章于 2020-08-28 17:00:00 发布