已编译的 "Hello World" C 程序如何使用机器语言存储字符串?
How does a compiled "Hello World" C program store the String using machine language?
所以我今天开始学习机器语言。我用 C 编写了一个基本的 "Hello World" 程序,它使用 for 循环打印了十次 "Hello, world!" 。然后我使用 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]不应该改变吗?
编译器'hard wires'将字符串转换为目标代码,然后链接器'hard wires'将其转换为机器代码。
并不是说该字符串嵌入到代码中,也没有存储在数据区域中,这意味着如果您获取指向该字符串的指针并试图更改它,您将得到一个异常。
我的二进制文件或程序是由机器代码和数据组成的。在这种情况下,您放入源代码中的字符串,编译器也只是字节数据,并且由于它的使用方式被视为只读数据,因此取决于可能落入 .rodata 或 .text 的编译器或编译器可能使用的其他名称。 Gcc 可能会称它为 .rodata。程序本身是.text。 linker 出现了,当它 links 的东西找到了 .text、.data、.bss、.rodata 和您可能拥有的任何其他项目的位置,然后将这些点连接起来。在你调用 printf 的情况下,linker 知道它把字符串、字节数组放在哪里,并且它被告知它的名字是什么(毫无疑问是一些内部临时名称)并且 printf 调用被告知linker 在调用 printf 之前修补指令以获取格式字符串的地址。
Disassembly of section .text:
0000000000400430 <main>:
400430: 53 push %rbx
400431: bb 0a 00 00 00 mov [=10=]xa,%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 [=10=]x4005e4,%edi
400445: e8 b6 ff ff ff callq 400400 <puts@plt>
40044a: 83 eb 01 sub [=10=]x1,%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 [=10=]x20,%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 [=11=]x4005e4,%edi
以便 linker 稍后可以填写。 gnu 反汇编程序试图反汇编没有意义的 .rodata(和 .data 等)块,因此请忽略它试图解释从地址 0x4005e4 开始的字符串的指令。
在 link 对象反汇编之前显示 .text 和 .rodata 这两个部分
Disassembly of section .text.startup:
0000000000000000 <main>:
0: 53 push %rbx
1: bb 0a 00 00 00 mov [=12=]xa,%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 [=12=]x0,%edi
15: e8 00 00 00 00 callq 1a <main+0x1a>
1a: 83 eb 01 sub [=12=]x1,%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 [=12=]x20,%al
7: 77 6f ja 78 <main+0x78>
9: 72 6c jb 77 <main+0x77>
b: 64 21 00 and %eax,%fs:(%rax)
unlinked 它必须填充这个 address/offset 以便 linker 稍后填写。
10: bf 00 00 00 00 mov [=13=]x0,%edi
另请注意,该对象仅包含 .rodata 中的字符串。 link与库和其他项目一起使它成为一个完整的程序显然添加了更多的 .rodata,但是 linker 管理所有这些。
也许用这个例子更容易理解
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 [=15=]x7,%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)
...
并且无论出于何种原因,您都必须 link 它才能看到 .bss 部分。该示例的要点是函数的机器代码在 .text 中,未初始化的全局变量在 .bss 中,初始化的全局变量是 .data,const 初始化的全局变量是 .rodata。编译器足够聪明,知道 const 即使它是全局的也不会改变,所以它可以将该值硬编码到数学中而不需要从 ram 中读取,但是它必须从 ram 中读取的其他两个变量因此生成一条指令地址零由 linker 在 link 时间填写。
在您的情况下,您读取的 only/const 数据是字节的集合,它不是数学运算,因此源文件中定义的字节被放置在内存中,因此它们可以作为第一个参数指向到 printf.
二进制不仅仅是机器代码。并且编译器和 linker 可以将东西放在内存中供机器代码获取,机器代码本身不必编写每个将被其余机器代码使用的值。
所以我今天开始学习机器语言。我用 C 编写了一个基本的 "Hello World" 程序,它使用 for 循环打印了十次 "Hello, world!" 。然后我使用 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]不应该改变吗?
编译器'hard wires'将字符串转换为目标代码,然后链接器'hard wires'将其转换为机器代码。
并不是说该字符串嵌入到代码中,也没有存储在数据区域中,这意味着如果您获取指向该字符串的指针并试图更改它,您将得到一个异常。
我的二进制文件或程序是由机器代码和数据组成的。在这种情况下,您放入源代码中的字符串,编译器也只是字节数据,并且由于它的使用方式被视为只读数据,因此取决于可能落入 .rodata 或 .text 的编译器或编译器可能使用的其他名称。 Gcc 可能会称它为 .rodata。程序本身是.text。 linker 出现了,当它 links 的东西找到了 .text、.data、.bss、.rodata 和您可能拥有的任何其他项目的位置,然后将这些点连接起来。在你调用 printf 的情况下,linker 知道它把字符串、字节数组放在哪里,并且它被告知它的名字是什么(毫无疑问是一些内部临时名称)并且 printf 调用被告知linker 在调用 printf 之前修补指令以获取格式字符串的地址。
Disassembly of section .text:
0000000000400430 <main>:
400430: 53 push %rbx
400431: bb 0a 00 00 00 mov [=10=]xa,%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 [=10=]x4005e4,%edi
400445: e8 b6 ff ff ff callq 400400 <puts@plt>
40044a: 83 eb 01 sub [=10=]x1,%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 [=10=]x20,%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 [=11=]x4005e4,%edi
以便 linker 稍后可以填写。 gnu 反汇编程序试图反汇编没有意义的 .rodata(和 .data 等)块,因此请忽略它试图解释从地址 0x4005e4 开始的字符串的指令。
在 link 对象反汇编之前显示 .text 和 .rodata 这两个部分
Disassembly of section .text.startup:
0000000000000000 <main>:
0: 53 push %rbx
1: bb 0a 00 00 00 mov [=12=]xa,%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 [=12=]x0,%edi
15: e8 00 00 00 00 callq 1a <main+0x1a>
1a: 83 eb 01 sub [=12=]x1,%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 [=12=]x20,%al
7: 77 6f ja 78 <main+0x78>
9: 72 6c jb 77 <main+0x77>
b: 64 21 00 and %eax,%fs:(%rax)
unlinked 它必须填充这个 address/offset 以便 linker 稍后填写。
10: bf 00 00 00 00 mov [=13=]x0,%edi
另请注意,该对象仅包含 .rodata 中的字符串。 link与库和其他项目一起使它成为一个完整的程序显然添加了更多的 .rodata,但是 linker 管理所有这些。
也许用这个例子更容易理解
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 [=15=]x7,%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)
...
并且无论出于何种原因,您都必须 link 它才能看到 .bss 部分。该示例的要点是函数的机器代码在 .text 中,未初始化的全局变量在 .bss 中,初始化的全局变量是 .data,const 初始化的全局变量是 .rodata。编译器足够聪明,知道 const 即使它是全局的也不会改变,所以它可以将该值硬编码到数学中而不需要从 ram 中读取,但是它必须从 ram 中读取的其他两个变量因此生成一条指令地址零由 linker 在 link 时间填写。
在您的情况下,您读取的 only/const 数据是字节的集合,它不是数学运算,因此源文件中定义的字节被放置在内存中,因此它们可以作为第一个参数指向到 printf.
二进制不仅仅是机器代码。并且编译器和 linker 可以将东西放在内存中供机器代码获取,机器代码本身不必编写每个将被其余机器代码使用的值。