Skip to content

Object file và ELF

Bài này mở file hello.o ra xem bên trong có gì. Đây là kiến thức nền để hiểu linker làm gì ở bài 3 và loader làm gì ở bài 4.

Ví dụ dùng lại hello.c và math.c ở bài 1.

ELF có hai "góc nhìn"

ELF (Executable and Linkable Format) là định dạng chung cho object file, executable, shared library và core dump trên Linux. Cùng một file ELF được nhìn theo hai cách:

Linking view Execution view
Đơn vị Section Segment (program header)
Ai dùng Linker, objdump, readelf -S Kernel/loader, readelf -l
Có trong .o, executable, .so executable, .so (không cần trong .o)

File .o chỉ có section. Executable có cả hai: loader dùng segment để biết cần map gì vào memory, còn section vẫn được giữ lại để phục vụ debug và phân tích (xem bài 4).

.o  (relocatable)           executable
┌──────────────────┐        ┌──────────────────┐
│ ELF header       │        │ ELF header       │
│ Section headers  │        │ Program headers  │ ◀─ execution view
│ .text .data ...  │        │ .text .data ...  │
│ .symtab .rela.*  │        │ Section headers  │ ◀─ linking view
└──────────────────┘        └──────────────────┘

Section

readelf -SW hello.o (rút gọn):

$ readelf -SW hello.o
  [Nr] Name              Type      Off    Size   Flg
  [ 1] .text             PROGBITS  000040 00004b  AX
  [ 2] .rela.text        RELA      0001f8 000060   I
  [ 3] .data             PROGBITS  00008b 000000  WA
  [ 4] .bss              NOBITS    00008b 000000  WA
  [ 5] .rodata           PROGBITS  000090 000018   A
  [ 6] .comment          PROGBITS  0000a8 00002e  MS
  [ 9] .eh_frame         PROGBITS  0000f8 000038   A
  [11] .symtab           SYMTAB    000130 0000a8
  [12] .strtab           STRTAB    0001d8 00001c
Section Nội dung Flag
.text machine code của các hàm AX (alloc, execute)
.rodata hằng, chuỗi literal A
.data biến global/static đã khởi tạo giá trị khác 0 WA (write, alloc)
.bss biến global/static chưa khởi tạo hoặc bằng 0; chỉ lưu kích thước, không chiếm chỗ trong file (type NOBITS) WA
.symtab / .strtab bảng symbol và bảng tên
.rela.text relocation cho .text
.eh_frame thông tin unwind stack (exception, backtrace) A
.comment phiên bản compiler

Có thể xem nội dung .rodata của hello.o:

$ objdump -s -j .rodata hello.o
Contents of section .rodata:
 0000 61726561 203d2025 660a0000 00000000  area = %f.......
 0010 6e861bf0 f9210940                    n....!.@

16 byte đầu là chuỗi "area = %f\n"; 8 byte sau là hằng 3.14159 ở dạng double (IEEE 754), do compiler đưa vào chứ không phải do ta khai báo thành biến.

size cho cái nhìn tổng quát về ba vùng chính:

$ size hello.o
   text    data     bss     dec     hex filename
    187       0       0     187      bb hello.o

size gộp section

Cột text của size gồm .text, .rodata và vài section chỉ đọc khác, không chỉ riêng .text. Với hello.o: .text (0x4b) + .rodata (0x18) + .eh_frame (0x38) + .note.gnu.property (0x20) = 187.

Biến nằm ở section nào

int  a = 5;          // .data
int  b;              // .bss
int  c = 0;          // .bss  (khởi tạo bằng 0 được tối ưu vào .bss)
const int d = 7;     // .rodata
static int e = 9;    // .data  (nhưng symbol là LOCAL)

void f(void) {
    int x;           // không ở section nào: stack, tồn tại lúc chạy
    static int y;    // .bss
    char *p = "hi";  // p: stack; chuỗi "hi": .rodata
}

Biến local thường (automatic) không nằm trong object file, vì chúng chỉ tồn tại trên stack lúc hàm chạy.

Symbol table

readelf -sW hello.o:

$ readelf -sW hello.o
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
     2: 00000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 00000000     0 SECTION LOCAL  DEFAULT    5 .rodata
     4: 00000000    75 FUNC    GLOBAL DEFAULT    1 main
     5: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND square
     6: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND printf

Các cột đáng chú ý:

  • Ndx: section chứa symbol. UND (undefined) nghĩa là symbol này được dùng nhưng nằm ở file khác: đây là "món nợ" mà linker phải trả. main ở section 1 (.text) nên là symbol được định nghĩa.
  • Bind: LOCAL (chỉ thấy trong file này, ví dụ biến/hàm static), GLOBAL (cho file khác thấy), WEAK (xem bài 3).
  • Type: FUNC, OBJECT (biến), SECTION, FILE, NOTYPE.
  • Size: main dài 75 byte (0x4b), bằng kích thước .text.

nm là cách xem gọn hơn của cùng bảng này. Mỗi ký tự là loại symbol, chữ hoa là global, chữ thường là local:

Ký tự Ý nghĩa
T / t .text
D / d .data
B / b .bss
R / r .rodata
U undefined
W / w weak
$ nm a.o
0000000000000000 D counter     # int counter = 1;        global, .data
0000000000000000 T get_hidden
0000000000000004 d hidden      # static int hidden = 5;  local,  .data

Tìm hiểu nhanh một symbol

nm -C file.o giải mã tên C++ (name mangling); nm -S --size-sort file.o sắp theo kích thước, hữu ích để tìm hàm/biến "ngốn" chỗ nhất.

Relocation

Assembler không biết địa chỉ cuối cùng của symbol, nên với mỗi chỗ cần địa chỉ nó để trống và ghi một relocation entry:

$ readelf -rW hello.o
Relocation section '.rela.text' at offset 0x1f8 contains 4 entries:
    Offset             Type               Symbol's Name + Addend
000000000000000e  R_X86_64_PLT32         square - 4
000000000000001e  R_X86_64_PC32          .rodata + c
0000000000000033  R_X86_64_PC32          .rodata - 4
0000000000000040  R_X86_64_PLT32         printf - 4

Mỗi entry gồm: offset (vị trí cần vá trong .text), symbol (cần địa chỉ của gì), type (cách tính giá trị) và addend (hằng số cộng thêm).

  • Hai entry R_X86_64_PLT32 ứng với hai lệnh call tới square và printf (đã thấy e8 00 00 00 00 ở bài 1).
  • Hai entry R_X86_64_PC32 trỏ vào .rodata: entry ở offset 0x1e (.rodata + 0xc, tức đích là .rodata + 0x10 sau khi bù addend) là hằng 3.14159, entry ở offset 0x33 (.rodata - 4, đích là .rodata + 0) là chuỗi "area = %f\n". Đây là PC-relative: giá trị điền vào là khoảng cách từ lệnh đến đích, nên code không phụ thuộc vị trí tuyệt đối.
  • Addend -4 có mặt vì CPU tính offset từ lệnh kế tiếp, tức sau 4 byte operand.

Công thức cho kiểu PC-relative: giá trị = S + A - P, với S là địa chỉ symbol, A là addend, P là địa chỉ của chỗ đang vá. Ví dụ với call square ở bài 3: S = 0x1194, A = -4, P = 0x1157 cho ra 0x1194 - 4 - 0x1157 = 0x39, đúng giá trị 39 00 00 00 trong executable.

Mỗi kiến trúc có bộ relocation type riêng (R_ARM_CALL, R_AARCH64_CALL26, ...). Đây là lý do object file của x86-64 không thể link với object file ARM.

Tóm tắt

  • Object file là ELF relocatable, chỉ có section; executable thêm program header cho loader.
  • Section chính: .text (code), .rodata, .data, .bss (không chiếm chỗ trong file).
  • Symbol table cho biết file định nghĩa gì (T, D, ...) và cần gì (U).
  • Relocation entry đánh dấu những chỗ linker phải điền địa chỉ thật.

Tiếp theo: Linking.

References

  • man elf, man readelf, man nm
  • System V ABI / ELF specification
  • Randal E. Bryant, David R. O'Hallaron - Computer Systems: A Programmer's Perspective, mục 7.4-7.7