1

所以我今天开始学习机器语言。我用 C 语言编写了一个基本的“Hello World”程序,它打印“Hello, world!”。十次使用 for 循环。然后我使用 Gnu Debugger 反汇编 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 指向更改时,地址 [esp+0x1c] 不应该更改吗?

4

2 回答 2

2

二进制或程序由机器代码和数据组成。在这种情况下,您放入源代码的字符串,编译器也只是字节的数据,并且由于它的使用方式被认为是只读数据,因此取决于可能落在 .rodata 或 .text 中的编译器或编译器可能使用的其他名称。Gcc 可能会称它为 .rodata。程序本身是 .text 格式的。链接器出现,当它链接事物时,它会为 .text、.data、.bss、.rodata 和您可能拥有的任何其他项目找到一个位置,然后将这些点连接起来。在你调用 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,const 初始化的全局是 .rodata。编译器足够聪明,知道一个 const 即使它是全局的也不会改变,所以它可以将该值硬编码到数学中,而不需要从 ram 中读取,但是它必须从 ram 中读取另外两个变量,因此会生成一条指令链接器在链接时填写地址零。

在您的情况下,您的只读/常量数据是字节的集合,它不是数学运算,因此源文件中定义的字节被放置在内存中,因此它们可以作为 printf 的第一个参数指向。

二进制文件不仅仅是机器代码。并且编译器和链接器可以将东西放在内存中以供机器代码获取,机器代码本身不必编写将由机器代码的其余部分使用的每个值。

于 2017-03-06T20:34:43.473 回答
0

编译器将字符串“硬连线”到目标代码中,然后链接器将其“硬连线”到机器代码中。

并不是说字符串嵌入到代码中,也没有存储在数据区域中,这意味着如果您使用指向该字符串的指针并尝试更改它,您会得到一个异常。

于 2017-03-06T20:06:14.653 回答