Как скомпилированная программа Hello World C хранит строку с использованием машинного языка?

поэтому сегодня я начал изучать машинный язык. Я написал базовую программу «Hello World» на C, которая печатает «Hello, world!» десять раз, используя цикл for. Затем я использовал отладчик Gnu для дизассемблирования main и просмотра кода на машинном языке (мой компьютер имеет процессор x86, и я настроил gdb для использования синтаксиса Intel):

user@PC:~/Path/To/Code$ gdb -q ./a.out
Reading symbols from ./a.out...done.
(gdb) list
1      #include <stdio.h>
2
3      int main()
4      {
5           int i;
6           for(i = 0; i < 10; i++) {
7                printf("Hello, world!\n");
8           }
9           return 0;
10      } 
(gdb) disassemble main
Dump of assembler code for function main:
  0x0804841d <+0>:     push    ebp
  0x0804841e <+1>:     mov     ebp,esp
  0x08048420 <+3>:     and     esp,0xfffffff0
  0x08048423 <+6>:     sub     esp,0x20
  0x08048426 <+9>:     mov     DWORD PTR [esp+0x1c],0x0
  0x0804842e <+17>:    jmp     0x8048441 <main+36>
  0x08048430 <+19>:    mov     DWORD PTR [esp],0x80484e0
  0x08048437 <+26>:    call    0x80482f0 <puts@plt>
  0x0804843c <+31>:    add     DWORD PTR [esp+0x1c],0x1
  0x08048441 <+36>:    cmp     DWORD PTR [esp+0x1c],0x9
  0x08048446 <+41>:    jle     0x8048430 <main+19>
  0x08048448 <+43>:    mov     eax,0x0
  0x0804844d <+48>:    leave
  0x0804844e <+49>:    ret
End of assembler dump.
(gdb) x/s 0x80484e0
0x80484e0: "Hello, world!"

Я понимаю большую часть машинного кода и то, что делает каждая из команд. Если я правильно понял, адрес "0x80484e0" загружается в регистр esp, чтобы можно было использовать память по этому адресу. Я изучил адрес, и неудивительно, что он содержал нужную строку. Теперь мой вопрос: как эта строка вообще попала туда? Я не могу найти часть программы, которая устанавливает строку в этом месте.

Я также не понимаю еще кое-что: когда я впервые запускаю программу, eip указывает на , где переменная i инициализируется в [esp+0x1c]. Однако адрес, на который указывает esp, позже изменяется в программе (на 0x80484e0), но [esp+0x1c] по-прежнему используется для «i» после этого изменения. Разве адрес [esp+0x1c] не должен меняться, когда адрес esp указывает на изменения?


person Keno Goertz    schedule 06.03.2017    source источник
comment
Он находится в вашем двоичном файле, когда ОС запускает вашу программу, она загружается в память, как и машинный код вашей программы. Обратите внимание, что [esp] не совпадает с esp, первый обращается к памяти и не изменяет самого esp.   -  person Jester    schedule 06.03.2017
comment
Загружается в память. Я предполагаю, что адрес памяти этой строки начинается с 0x8048441 и заканчивается на 0x80484e0. Имейте в виду, что строка — это список целых чисел.   -  person Xorifelse    schedule 06.03.2017
comment
@Jester А, так mov DWORD PTR [esp], 0x80484e0 на самом деле не указывает esp на новый адрес, а просто записывает 0x80484e0 в адрес, на который он сейчас указывает?   -  person Keno Goertz    schedule 06.03.2017
comment
Да, это именно то, что он делает. В этом соглашении о вызовах аргументы передаются в стеке. Это будет аргументом для puts.   -  person Jester    schedule 06.03.2017
comment
Программа не устанавливает строку в этом месте, это процесс компиляции/компоновки, который помещает "Hello, world!\n" в раздел данных только для чтения, будучи строковым литералом. Этот возможно дублирующийся вопрос может вас заинтересовать.   -  person Weather Vane    schedule 06.03.2017
comment
Вы можете использовать objdump -d -M intel -s a.out, чтобы увидеть также раздел .data, в котором хранится строка (редактировать: НЕ в вашем случае, переходит в .text с кодом). Затем эта часть исполняемого файла загружается в память в виде двоичного блока перед запуском первой инструкции исполняемого файла, поэтому код найдет эту часть памяти, настроенную со значениями из источника. (на самом деле строка находится в сегменте кода, так что objdump будет немного забавнее, чем когда вы делаете это с помощью простого человеческого примера ASM hello world... попробуйте в любом случае :) )   -  person Ped7g    schedule 07.03.2017


Ответы (2)


Двоичный файл или программа состоит как из машинного кода, так и из данных. В этом случае ваша строка, которую вы поместили в исходный код, компилятор тоже использует данные, которые представляют собой просто байты, и из-за того, как она использовалась, считалась данными только для чтения, поэтому в зависимости от компилятора, который может попасть в .rodata или .text или какое-либо другое имя, которое может использовать компилятор. Gcc, вероятно, назвал бы это .rodata. Сама программа в .text. Появляется компоновщик, и когда он связывает вещи, он находит место для .text, .data, .bss, .rodata и любых других элементов, которые могут у вас быть, а затем соединяет точки. В случае вашего вызова printf компоновщик знает, куда он поместил строку, массив байтов, и ему было сказано, каково его имя (без сомнения, какое-то внутреннее временное имя), и вызову printf было сказано об этом имени, чтобы компоновщик исправляет инструкцию, чтобы захватить адрес в строку формата перед вызовом printf.

Disassembly of section .text:

0000000000400430 <main>:
  400430:   53                      push   %rbx
  400431:   bb 0a 00 00 00          mov    $0xa,%ebx
  400436:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  40043d:   00 00 00 
  400440:   bf e4 05 40 00          mov    $0x4005e4,%edi
  400445:   e8 b6 ff ff ff          callq  400400 <puts@plt>
  40044a:   83 eb 01                sub    $0x1,%ebx
  40044d:   75 f1                   jne    400440 <main+0x10>
  40044f:   31 c0                   xor    %eax,%eax
  400451:   5b                      pop    %rbx
  400452:   c3                      retq   
  400453:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  40045a:   00 00 00 
  40045d:   0f 1f 00                nopl   (%rax)



Disassembly of section .rodata:

00000000004005e0 <_IO_stdin_used>:
  4005e0:   01 00                   add    %eax,(%rax)
  4005e2:   02 00                   add    (%rax),%al
  4005e4:   48                      rex.W
  4005e5:   65 6c                   gs insb (%dx),%es:(%rdi)
  4005e7:   6c                      insb   (%dx),%es:(%rdi)
  4005e8:   6f                      outsl  %ds:(%rsi),(%dx)
  4005e9:   2c 20                   sub    $0x20,%al
  4005eb:   77 6f                   ja     40065c <__GNU_EH_FRAME_HDR+0x68>
  4005ed:   72 6c                   jb     40065b <__GNU_EH_FRAME_HDR+0x67>
  4005ef:   64 21 00                and    %eax,%fs:(%rax)

компилятор закодировал эту инструкцию, но оставил адрес как нули, вероятно, или какое-то заполнение

  400440:   bf e4 05 40 00          mov    $0x4005e4,%edi

чтобы компоновщик мог заполнить его позже. Дизассемблер gnu пытается дизассемблировать блоки .rodata (и .data и т. д.), что не имеет смысла, поэтому игнорируйте инструкции, которые он пытается интерпретировать, ваша строка, начинающаяся с адреса 0x4005e4.

Перед привязкой разборки объекта показаны два раздела .text и .rodata

Disassembly of section .text.startup:

0000000000000000 <main>:
   0:   53                      push   %rbx
   1:   bb 0a 00 00 00          mov    $0xa,%ebx
   6:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
   d:   00 00 00 
  10:   bf 00 00 00 00          mov    $0x0,%edi
  15:   e8 00 00 00 00          callq  1a <main+0x1a>
  1a:   83 eb 01                sub    $0x1,%ebx
  1d:   75 f1                   jne    10 <main+0x10>
  1f:   31 c0                   xor    %eax,%eax
  21:   5b                      pop    %rbx
  22:   c3                      retq   

0000000000000000 <.rodata.str1.1>:
   0:   48                      rex.W
   1:   65 6c                   gs insb (%dx),%es:(%rdi)
   3:   6c                      insb   (%dx),%es:(%rdi)
   4:   6f                      outsl  %ds:(%rsi),(%dx)
   5:   2c 20                   sub    $0x20,%al
   7:   77 6f                   ja     78 <main+0x78>
   9:   72 6c                   jb     77 <main+0x77>
   b:   64 21 00                and    %eax,%fs:(%rax)

несвязанный, он должен просто дополнить этот адрес/смещение, чтобы компоновщик мог заполнить его позже.

  10:   bf 00 00 00 00          mov    $0x0,%edi

также обратите внимание, что объект содержит только строку в .rodata. связывание с библиотеками и другими элементами, чтобы сделать его полной программой, явно добавило больше .rodata, но компоновщик управляет всем этим.

Возможно, проще увидеть на этом примере

void more_fun ( unsigned int, unsigned int, unsigned int );

unsigned int a;
unsigned int b=5;
const unsigned int c=7;

void fun ( void )
{
    more_fun(a,b,c);
}

разобран как объект

Disassembly of section .text:

0000000000000000 <fun>:
   0:   8b 35 00 00 00 00       mov    0x0(%rip),%esi        # 6 <fun+0x6>
   6:   8b 3d 00 00 00 00       mov    0x0(%rip),%edi        # c <fun+0xc>
   c:   ba 07 00 00 00          mov    $0x7,%edx
  11:   e9 00 00 00 00          jmpq   16 <fun+0x16>

Disassembly of section .data:

0000000000000000 <b>:
   0:   05                      .byte 0x5
   1:   00 00                   add    %al,(%rax)
    ...

Disassembly of section .rodata:

0000000000000000 <c>:
   0:   07                      (bad)  
   1:   00 00                   add    %al,(%rax)
    ...

и по какой-то причине вы должны связать его, чтобы увидеть раздел .bss. Суть примера в том, что машинный код для функции находится в .text, неинициализированный глобальный файл — в .bss, инициализированный глобальный — .data, а константный глобальный — .rodata. Компилятор был достаточно умен, чтобы знать, что константа, даже если она глобальная, не изменится, поэтому он может просто жестко закодировать это значение в математике и не читать из оперативной памяти, но две другие переменные он должен прочитать из оперативной памяти, поэтому генерирует инструкцию с адресными нулями, которые должны быть заполнены компоновщиком во время компоновки.

В вашем случае ваши данные только для чтения / const представляли собой набор байтов, и это не была математическая операция, поэтому байты, определенные в исходном файле, были помещены в память, чтобы на них можно было указать в качестве первого параметра для printf.

Двоичный код — это нечто большее, чем просто машинный код. И компилятор, и компоновщик могут размещать вещи в памяти для получения машинным кодом, сам машинный код не должен записывать каждое значение, которое будет использоваться остальной частью машинного кода.

person old_timer    schedule 06.03.2017
comment
Было бы лучше отображать родаты раздела объекта в виде данных, dw 1,2 db "Hello, World!", вместо того, чтобы использовать дизассемблирование текста, которое создавало странную последовательность инструкций. - person rcgldr; 07.03.2017
comment
конечно, но тогда код не так чист для чтения (-s вместо -c при компиляции), важно понимать, что это просто байты массив байтов... процессор не видит ascii... - person old_timer; 07.03.2017
comment
Я предполагаю, что кто-то выше уже упоминал -s, чтобы увидеть, что происходит ... если нет, то скомпилируйте с -s или используйте -save-temps, и сборка, которая передается ассемблеру, не будет удалена. - person old_timer; 07.03.2017

Компилятор «жестко связывает» строку с объектным кодом, а компоновщик затем «жестко связывает» ее с машинным кодом.

Дело не в том, что строка встроена в код и не хранится в области данных, что означает, что если вы возьмете указатель на строку и попытаетесь ее изменить, вы получите исключение.

person Quandon    schedule 06.03.2017
comment
Это снова может быть исправлено во время выполнения, чтобы затруднить работу хакеров. - person Weather Vane; 06.03.2017