Это как понять? - PullRequest
       29

Это как понять?

6 голосов
/ 03 апреля 2011

Это от этот вопрос .

 gcc -c test.s
 objcopy -O binary test.o test.bin

В чем разница между test.o и test.bin?

.text
    call start
    str:
        .string "test\n"
    start:
    movl    $4, %eax
    movl    $1, %ebx
    pop     %ecx
    movl    $5, %edx
    int     $0x80
    ret

Что выше делает?

Ответы [ 2 ]

10 голосов
/ 04 апреля 2011

objcopy -O binary копирует содержимое исходного файла. Здесь test.o - это «перемещаемый объектный файл»: это код, а также таблица символов и информация о перемещении, позволяющая связать файл с другими файлами в исполняемой программе. Файл test.bin, созданный objcopy, содержит только код, без таблицы символов или информации о перемещении. Такой «сырой» файл бесполезен для «нормального» программирования, но удобен для кода, который имеет собственный загрузчик.

Я предполагаю, что вы используете Linux в 32-битной системе x86. Ваш test.o файл имеет размер 515 байт. Если вы попробуете objdump -x test.o, вы получите следующее, описывающее содержимое объектного файла test.o:

$ objdump -x test.o

test.o:     file format elf32-i386
test.o
architecture: i386, flags 0x00000010:
HAS_SYMS
start address 0x00000000

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         0000001e  00000000  00000000  00000034  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000000  00000000  00000000  00000054  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  00000000  00000000  00000054  2**2
                  ALLOC
SYMBOL TABLE:
00000000 l    d  .text  00000000 .text
00000000 l    d  .data  00000000 .data
00000000 l    d  .bss   00000000 .bss
0000000b l       .text  00000000 start
00000005 l       .text  00000000 str

Это дает вам довольно много информации. В частности, файл содержит раздел с именем .text, начинающийся со смещения 0x34 в файле (это 52 в десятичном виде) и длиной 0x1e байтов (30 в десятичном формате). Вы можете разобрать его, чтобы увидеть сами коды операций:

$ objdump -d test.o

test.o:     file format elf32-i386


Disassembly of section .text:

00000000 <str-0x5>:
   0:   e8 06 00 00 00          call   b <start>

00000005 <str>:
   5:   74 65                   je     6c <start+0x61>
   7:   73 74                   jae    7d <start+0x72>
   9:   0a 00                   or     (%eax),%al

0000000b <start>:
   b:   b8 04 00 00 00          mov    $0x4,%eax
  10:   bb 01 00 00 00          mov    $0x1,%ebx
  15:   59                      pop    %ecx
  16:   ba 05 00 00 00          mov    $0x5,%edx
  1b:   cd 80                   int    $0x80
  1d:   c3                      ret    

Это более или менее сборка, с которой вы начали. Коды операций je, jae и or в середине являются ложными: это objdump пытается интерпретировать буквенную строку ("test\n", в результате чего байты 0x74 0x65 0x73 0x64 0x0a 0x00) как коды операций. objdump -d также показывает фактические байты, найденные в разделе .text, то есть байты в файле, начинающиеся со смещения 0x34. Первые байты 0xe8 0x06 0x00 ...

Теперь взгляните на файл test.bin. Имеет длину 30 байт. Давайте посмотрим на эти байты в шестнадцатеричном формате:

$ hd test.bin
00000000  e8 06 00 00 00 74 65 73  74 0a 00 b8 04 00 00 00  |.....test.......|
00000010  bb 01 00 00 00 59 ba 05  00 00 00 cd 80 c3        |.....Y........|

мы распознаем здесь ровно 30 байтов из секции .text в test.o. Вот что сделал objcopy -O binary: он извлек содержимое файла, то есть единственный непустой раздел, то есть сами необработанные коды операций, удалив все остальное, в частности таблицу символов и информацию о перемещении.

Перемещение - это то, что должно быть изменено в данном фрагменте кода, чтобы оно правильно работало при хранении в заданном месте в памяти. Например, если код использует переменную и хочет получить адрес этой переменной, то информация о перемещении будет содержать запись, сообщающую тому, кто фактически поместит код в память (обычно это компоновщик): msgstr "здесь в коде, когда вы знаете, где переменная будет на самом деле, напишите адрес переменной". Интересно, что код, который вы показываете, не нуждается в перемещении: последовательность байтов может быть записана в произвольном месте памяти и выполнена как есть.

Давайте посмотрим, что делает код.

  • Код операции call переходит к инструкции mov со смещением 0x0b. Кроме того, поскольку это call, он помещает в стек адрес возврата. Обратный адрес - это место, где выполнение должно продолжаться после завершения вызова, то есть при достижении кода операции ret. Это адрес байта, следующего за кодом операции call. Здесь этот адрес является адресом первого байта литеральной строки "test\n".
  • Две movl нагрузки %eax и %ebx с числовыми значениями 4 и 1 соответственно.
  • Код операции pop удаляет верхний элемент из стека, сохраняя его в %ecx. Что это за верхний элемент? Именно этот адрес помещается в стек с помощью кода операции call, то есть адрес первого байта литеральной строки.
  • Третий movl загружает %edx с числовым значением 5.
  • int $0x80 - системный вызов в 32-битном x86 Linux: это вызывает ядро. Ядро будет смотреть на регистры, чтобы знать, что делать. Сначала ядро ​​смотрит на %eax, чтобы получить «номер системного вызова»; на 32-битном x86 "4" - это __NR_write, то есть системный вызов write(). Этот вызов ожидает три параметра в регистрах %ebx, %ecx и %edx в указанном порядке. Это дескриптор файла назначения (здесь 1: это стандартный вывод), указатель на данные для записи (здесь литеральная строка) и длина данных для записи (здесь 5, что соответствует четырем буквам и символу новой строки персонаж). Так что это пишет "test\n" на стандартный вывод.
  • Финал ret возвращается звонящему.ret извлекает значение из стека и переходит на этот адрес.Это предполагает, что этот фрагмент кода был вызван с кодом операции call.

Итак, чтобы подвести итог, код печатает test с новой строкой.

Давайте попробуем этос пользовательским загрузчиком:

#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

int
main(void)
{
        void *p;
        int f;

        p = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        f = open("test.bin", O_RDONLY);
        read(f, p, 30);
        close(f);
        mprotect(p, 30, PROT_READ | PROT_EXEC);
        ((void (*)(void))p)();
        return 0;
}

(Приведенный выше код не проверяет возвращаемые значения на ошибки, что очень плохо, конечно.)

Здесь я выделяю страницу памяти (4096 байт) с mmap(), запрашивая страницу, на которой я могу читать и писать.p указывает на этот кусок.Затем с помощью open(), read() и close() я считываю содержимое файла test.bin (30 байт) в этот фрагмент.

Вызов mprotect() дает ядру команду изменитьправа доступа к моей странице: сейчас я хочу иметь возможность выполнять эти байты, т.е. рассматривать их как машинный код.Я отказываюсь от права записи в чанк (в зависимости от точной конфигурации ядра наличие страницы, которая может быть как записана, так и исполнена, может быть запрещено).

Зашифрованный ((void (*)(void))p)(); читается так: Iвзять p;Я приведу его как указатель на функцию, которая не принимает аргументов и ничего не возвращает;Я вызываю эту функцию.Это синтаксис C для создания call в моем фрагменте данных.

Когда я запускаю эту программу, я получаю:

$ ./blah
test

, что и ожидалось: код в test.bin записывает test на стандартный вывод.

0 голосов
/ 03 апреля 2011

.o - предварительный компоновщик, а .bin - постлинкерный. Вот статья в википедии о компоновщиках: http://en.wikipedia.org/wiki/Linker_(computing) Я уверен, что вы можете получить точку отсюда:)

...