编译的“Hello World”C程序如何使用机器语言存储String?

时间:2017-03-06 20:02:46

标签: c assembly machine-language

所以我今天开始学习机器语言。我用C写了一个基本的“Hello World”程序,打印出“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指向更改时,地址[esp + 0x1c]是否应该更改?

2 个答案:

答案 0 :(得分: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,const初始化的全局是.rodata。编译器足够聪明,知道const即使它是全局的也不会改变所以它只能将该值硬编码到数学中而不需要从ram读取,但是它必须从ram读取的其他两个变量因此生成一条指令在链接时由链接器填充地址零。

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

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

答案 1 :(得分:0)

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

不是字符串嵌入到代码中,也不是存储在数据区中,这意味着如果你接受了指向字符串的指针并试图改变它,你就会得到一个异常。