Где в объектном файле начинается код функции main? - PullRequest
0 голосов
/ 29 мая 2020

У меня есть объектный файл программы C, которая печатает hello world, просто на вопрос. Я пытаюсь понять, используя утилиту readelf, gdb или hexedit (я не могу понять, какой инструмент правильный), где в файле запускается код функции «main».

Я знаю, что с помощью readelf появляется символ _start & main и адрес, по которому он отображается в виртуальной памяти. Более того, я также знаю, каков размер раздела .text и раздела, в котором указана точка входа, т.е. адрес, который совпадает с адресом текстового раздела.

Возникает вопрос - где в файле начинается код функции main? Я подумал, что это точка входа и смещение текстового раздела, но насколько я понимаю, данные разделов, bss, rodata должны запускаться перед основным, и они появляются после текста раздела в readelf.

* 1006 должен суммировать размер всех строк до основного в таблице символов, но я совсем не уверен, правильно ли он.

Дополнительный вопрос, который следует за этим: хочу ли я заменить основную функцию на NOP instrcutres или поместите одну инструкцию ret в мой объектный файл. как мне узнать смещение, где я могу это сделать, используя hexedit.

Ответы [ 2 ]

2 голосов
/ 01 июня 2020

_start обычно принадлежит модулю (файл *.o), который является фиксированным (в разных системах он называется по-разному, но общее имя crt0.o, которое написано на ассемблере). Этот фиксированный код подготавливает стек (обычно аргументы и окружение сохраняются в начальном сегменте стека с помощью системного вызова execve(2)) задача crt0.s - подготовить начальный кадр стека C и вызвать main(). После завершения main() он отвечает за получение возвращаемого значения из main и вызов всех обработчиков atexit() для fini sh, вызывая системный вызов _exit(2).

Связывание crt0.o является обычно прозрачно из-за того, что вы всегда вызываете компилятор, чтобы выполнить связывание, поэтому обычно вам не нужно добавлять crt0.o в качестве первого объектного модуля, но компилятор знает (в последнее время все это значительно выросло, поскольку мы зависим от архитектуры и ABI для передачи параметров между функциями)

Если вы запустите компилятор с параметром -v, вы получите точную командную строку, которую он использует для вызова компоновщика, и вы получите секреты окончательной карты памяти вашей программы на первых этапах.

2 голосов
/ 30 мая 2020

Итак, давайте go рассмотрим его шаг за шагом.

Начните с этого C файла:

#include <stdio.h>

void printit()
{
    puts("Hello world!");
}

int main(void)
{
    printit();
    return 0;
}

Поскольку комментарии выглядят так, как будто вы находитесь на x86, скомпилируйте его как 32-битный исполняемый файл, отличный от P IE, например:

$ gcc -m32 -no-pie  -o test test.c

Требуется опция -m32, потому что я работаю на машине x86-64. Как вы уже знаете, вы можете получить адрес виртуальной памяти main, используя readelf, objdump или nm, например, так:

$ nm test | grep -w main
0804918d T main

Очевидно, 804918d не может быть смещением в файле, который всего 15 КБ. Вам нужно найти соответствие между адресами виртуальной памяти и смещениями файлов . В типичном файле ELF отображение включается дважды . Один раз в подробной форме для компоновщиков (поскольку объектные файлы также являются файлами ELF) и отладчиков, а второй раз в сжатой форме, которая используется ядром для загрузки программ. Подробная форма - это список разделов, состоящий из заголовков разделов, и вы можете просмотреть его так (вывод немного сокращен, чтобы сделать ответ более читаемым):

$ readelf --section-headers test
There are 29 section headers, starting at offset 0x3748:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
[...]
  [11] .init             PROGBITS        08049000 001000 000020 00  AX  0   0  4
  [12] .plt              PROGBITS        08049020 001020 000030 04  AX  0   0 16
  [13] .text             PROGBITS        08049050 001050 0001c1 00  AX  0   0 16
  [14] .fini             PROGBITS        08049214 001214 000014 00  AX  0   0  4
  [15] .rodata           PROGBITS        0804a000 002000 000015 00   A  0   0  4
[...]
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

Здесь вы обнаружите, что Раздел .text начинается с (виртуального) адреса 08049050 и имеет размер 1c1 байт, поэтому он заканчивается по адресу 08049211. Адрес main, 804918d, находится в этом диапазоне, поэтому вы знаете, что main является членом текстового раздела. Если вы вычтите основание текстового раздела из адреса main, вы обнаружите, что main составляет 13d байт в текстовом разделе. Список раздела также содержит смещение файла, с которого начинаются данные для текстового раздела. Это 1050, поэтому первый байт main находится по смещению 0x1050 + 0x13d == 0x118d.

Вы можете сделать то же самое вычисление, используя заголовки программы:

$ readelf --program-headers test
[...]
Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x00160 0x00160 R   0x4
  INTERP         0x000194 0x08048194 0x08048194 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x002e8 0x002e8 R   0x1000
  LOAD           0x001000 0x08049000 0x08049000 0x00228 0x00228 R E 0x1000
  LOAD           0x002000 0x0804a000 0x0804a000 0x0019c 0x0019c R   0x1000
  LOAD           0x002f0c 0x0804bf0c 0x0804bf0c 0x00110 0x00114 RW  0x1000
[...]

Вторая строка загрузки сообщает вам, что область от 08049000 (VirtAddr) до 08049228 (VirtAddr + MemSiz) является читаемой и исполняемой и загружается со смещения 1000 в файле. Итак, вы снова можете вычислить, что адрес main составляет 18d байт в этой области загрузки, поэтому он должен находиться по смещению 0x118d внутри исполняемого файла. Давайте проверим, что:

$ ./test
Hello world!
$ echo -ne '\xc3' | dd of=test conv=notrunc bs=1 count=1 seek=$((0x118d))
1+0 records in
1+0 records out
1 byte copied, 0.0116672 s, 0.1 kB/s
$ ./test
$

Перезапись первого байта main с помощью 0xc3, кода операции return (near) на x86, приводит к тому, что программа больше ничего не выводит.

...