堆栈如何区分不同的数字类型?
How does the stack differentiate between different number types?
我正在尝试学习汇编,但在理解堆栈上的内存 allocation/retrieval 时遇到了一些困难。
当在堆栈上分配字符串时,程序知道在到达空终止字符时停止读取字符串 /x00
。然而,对于数字,没有这样的事情。程序如何知道堆栈上分配的数字的结尾,以及如何区分不同的数字类型(short、long、int)?
(我对此有点陌生,所以如果我有任何误解,请纠正我!)
How does the program know the end of a number allocated on the stack, and how does it differentiate between different number types (short, long, int)?
它在任何绝对意义上都不知道。也就是说,它无法通过查看那里的字节来确定驻留在堆栈(或内存中其他地方)的任何内容的类型。相反,程序必须知道在什么位置期望什么数据类型,并执行适合这些类型的指令。在某些情况下,它甚至可能根据不同的数据类型解释相同的数据。
Type(int
vs. float
vs. char *
vs. struct foo
)只有在翻译过程中才真正重要,当编译器正在分析您的源代码并将其转换为适当的机器代码时。那就是像“[]
的一个操作数应该是指针类型,另一个应该是整数类型”和“一元 *
的操作数应该是指针类型”和“乘法运算符的操作数”这样的规则应具有算术类型”等,被强制执行。
汇编语言通常处理字节、字(2 个字节)、长字(4 个字节)等,尽管一些专用平台可能有奇怪的字大小。操作码 addb
1 添加两个字节大小实体的内容, addl
添加两个长字大小实体的内容,等等。所以当编译器是翻译你的代码,它根据声明的类型为对象使用正确的操作码。因此,如果您将某些内容声明为 short
,编译器将(通常)使用用于字大小对象(addw
、movw
等)的操作码。如果您将某些内容声明为 int
或 long
,它将(通常)使用用于长字大小对象(addl
、movl
)的操作码。浮点类型通常使用一组不同的操作码和它们自己的一组寄存器来处理。
简而言之,汇编语言凭借编译器指定的操作码“知道”事物的位置和大小。
简单示例 - 这是一些使用 int
和 short
:
的 C 源代码
#include <stdio.h>
int main( void )
{
int x;
short y;
printf( "Gimme an x: " );
scanf( "%d", &x );
y = 2 * x + 30;
printf( "x = %d, y = %hd\n", x, y );
return 0;
}
我使用 -Wa,-aldh
选项和 gcc
来生成汇编代码列表,源代码交错,给我
GAS LISTING /tmp/cc3D25hf.s page 1
1 .file "simple.c"
2 .text
3 .Ltext0:
4 .section .rodata
5 .LC0:
6 0000 47696D6D .string "Gimme an x: "
6 6520616E
6 20783A20
6 00
7 .LC1:
8 000d 256400 .string "%d"
9 .LC2:
10 0010 78203D20 .string "x = %d, y = %hd\n"
10 25642C20
10 79203D20
10 2568640A
10 00
11 .text
12 .globl main
14 main:
15 .LFB0:
16 .file 1 "simple.c"
1:simple.c **** #include <stdio.h>
2:simple.c ****
3:simple.c **** int main( void )
4:simple.c **** {
17 .loc 1 4 0
18 .cfi_startproc
19 0000 55 pushq %rbp
20 .cfi_def_cfa_offset 16
21 .cfi_offset 6, -16
22 0001 4889E5 movq %rsp, %rbp
23 .cfi_def_cfa_register 6
24 0004 4883EC10 subq , %rsp
5:simple.c **** int x;
6:simple.c **** short y;
7:simple.c ****
8:simple.c **** printf( "Gimme an x: " );
25 .loc 1 8 0
26 0008 BF000000 movl $.LC0, %edi
26 00
27 000d B8000000 movl [=11=], %eax
27 00
28 0012 E8000000 call printf
28 00
9:simple.c **** scanf( "%d", &x );
29 .loc 1 9 0
30 0017 488D45F8 leaq -8(%rbp), %rax
31 001b 4889C6 movq %rax, %rsi
32 001e BF000000 movl $.LC1, %edi
32 00
33 0023 B8000000 movl [=11=], %eax
33 00
34 0028 E8000000 call __isoc99_scanf
34 00
10:simple.c ****
11:simple.c **** y = 2 * x + 30;
GAS LISTING /tmp/cc3D25hf.s page 2
35 .loc 1 11 0
36 002d 8B45F8 movl -8(%rbp), %eax
37 0030 83C00F addl , %eax
38 0033 01C0 addl %eax, %eax
39 0035 668945FE movw %ax, -2(%rbp)
12:simple.c ****
13:simple.c **** printf( "x = %d, y = %hd\n", x, y );
40 .loc 1 13 0
41 0039 0FBF55FE movswl -2(%rbp), %edx
42 003d 8B45F8 movl -8(%rbp), %eax
43 0040 89C6 movl %eax, %esi
44 0042 BF000000 movl $.LC2, %edi
44 00
45 0047 B8000000 movl [=11=], %eax
45 00
46 004c E8000000 call printf
46 00
14:simple.c **** return 0;
47 .loc 1 14 0
48 0051 B8000000 movl [=11=], %eax
48 00
15:simple.c **** }
49 .loc 1 15 0
50 0056 C9 leave
51 .cfi_def_cfa 7, 8
52 0057 C3 ret
53 .cfi_endproc
54 .LFE0:
56 .Letext0:
57 .file 2 "/usr/lib/gcc/x86_64-redhat-linux/7/include/stddef.h"
58 .file 3 "/usr/include/bits/types.h"
59 .file 4 "/usr/include/libio.h"
60 .file 5 "/usr/include/stdio.h"
如果你看线条
36 002d 8B45F8 movl -8(%rbp), %eax
37 0030 83C00F addl , %eax
38 0033 01C0 addl %eax, %eax
39 0035 668945FE movw %ax, -2(%rbp)
这是
的机器代码
y = 2 * x + 30;
当它处理 x
时,它使用长字的操作码:
movl -8(%rbp), %eax ;; copy the value in x to the eax register
addl , %eax ;; add the literal value 15 to the value in eax
addl %eax, %eax ;; multiply the value in eax by 2
当它处理 y
时,它使用单词的操作码:
movw %ax, -2(%rbp) ;; save the value in the lower 2 bytes of eax to y
这就是它“知道”要为给定对象读取多少字节的方式 - 所有这些信息都被写入机器代码本身。标量类型都有固定的已知大小,因此只需选择正确的操作码或使用操作码即可。
- 我使用的是 Intel 特定的助记符,但其他汇编器的概念是相同的。
TL;DR 程序员知道他们想要做什么,并在编程语言中通过变量和表达式表达出来and/or 语句。编译器生成的机器代码告诉处理器要做什么才能执行该程序。程序员通知程序(通过编写程序);程序通知编译器,编译器通知机器代码(通过翻译程序),机器代码在动态执行机器代码期间通知处理器。
高级编程语言根据变量以及操作变量的表达式或语句进行操作。变量是一种逻辑结构——变量通常在程序执行时创建和销毁。然而,处理器根据在存储上运行的机器代码指令来运行。存储是一种物理构造 — 存储只是一直存在,它不会被创建或销毁1,而是会被重复使用或改变用途。
此外,处理器不会读取这样的变量声明——机器代码中没有真正等同于变量声明的变量声明。因为处理器只读取机器代码指令,所以每条机器代码指令都必须通过机器代码指令告诉处理器它需要知道的关于用于变量的存储的所有信息。通过最少的错误检查,处理器简单地相信机器代码程序对存储做了一些有用的事情。
编程语言编译器的工作是将逻辑变量声明映射到物理存储保留中,并将表达式和语句翻译成机器代码指令,这些指令可以操纵保存程序变量的物理存储。
编译器读取并记住所有变量声明,因此当一行程序代码操作变量时,编译器知道要使用什么物理存储(通过查询其逻辑到物理映射)以及什么类型或用于该机器代码翻译的大小。对于赋值运算,通常真正需要的只是大小,但对于其他算术运算,还需要其他属性,例如signed-ness(signed vs. unsigned)或数字类型(integer vs. floating point)。
编译器理解程序,因为它是用定义明确的语言规范编写的,因此它可以读取变量声明和代码语句并将它们翻译成机器代码。由无错误编译器生成的机器代码始终以正确的方式使用给定变量的物理存储,给定程序的变量声明和语言规则的定义。这意味着编译器将使用机器代码指令进行翻译,这些机器代码指令按照程序员编写该程序的意图来操作物理存储。
因此,如果程序员将变量指定为 2 字节宽,编译器将识别(至少)2 字节的物理存储,并在该变量的生命周期内具有适当的持续时间,并且每当程序操作该变量时,编译器将生成机器代码来访问它用适当的大小和其他属性标识的物理存储。
堆栈只是一个内存区域,其使用几乎直接对应于 1:1 函数调用和调用链或调用堆栈。因此,它是放置生命周期限于函数持续时间的变量的好地方。因为函数的存储空间在其 return 时被释放,该物理存储空间可以被另一个新调用的函数重用。
堆栈内存上的操作(大部分)是使用与所有其他类型的内存相同的机器代码指令完成的——这些机器代码指令根据需要通知处理器大小和类型——只是堆栈是软件已知的由函数调用重新调整用途并由函数 return 释放。处理器对堆栈知之甚少。
(一些处理器有专门的 push
和 pop
指令以堆栈指针寄存器引用的内存为目标,这可以使执行这些操作比其他方式更有效)。
1(模虚拟内存但那是另一回事;计算机中的实际 RAM 和 CPU 寄存器都预先&post-在物理量不变的情况下存在程序)。
我正在尝试学习汇编,但在理解堆栈上的内存 allocation/retrieval 时遇到了一些困难。
当在堆栈上分配字符串时,程序知道在到达空终止字符时停止读取字符串 /x00
。然而,对于数字,没有这样的事情。程序如何知道堆栈上分配的数字的结尾,以及如何区分不同的数字类型(short、long、int)?
(我对此有点陌生,所以如果我有任何误解,请纠正我!)
How does the program know the end of a number allocated on the stack, and how does it differentiate between different number types (short, long, int)?
它在任何绝对意义上都不知道。也就是说,它无法通过查看那里的字节来确定驻留在堆栈(或内存中其他地方)的任何内容的类型。相反,程序必须知道在什么位置期望什么数据类型,并执行适合这些类型的指令。在某些情况下,它甚至可能根据不同的数据类型解释相同的数据。
Type(int
vs. float
vs. char *
vs. struct foo
)只有在翻译过程中才真正重要,当编译器正在分析您的源代码并将其转换为适当的机器代码时。那就是像“[]
的一个操作数应该是指针类型,另一个应该是整数类型”和“一元 *
的操作数应该是指针类型”和“乘法运算符的操作数”这样的规则应具有算术类型”等,被强制执行。
汇编语言通常处理字节、字(2 个字节)、长字(4 个字节)等,尽管一些专用平台可能有奇怪的字大小。操作码 addb
1 添加两个字节大小实体的内容, addl
添加两个长字大小实体的内容,等等。所以当编译器是翻译你的代码,它根据声明的类型为对象使用正确的操作码。因此,如果您将某些内容声明为 short
,编译器将(通常)使用用于字大小对象(addw
、movw
等)的操作码。如果您将某些内容声明为 int
或 long
,它将(通常)使用用于长字大小对象(addl
、movl
)的操作码。浮点类型通常使用一组不同的操作码和它们自己的一组寄存器来处理。
简而言之,汇编语言凭借编译器指定的操作码“知道”事物的位置和大小。
简单示例 - 这是一些使用 int
和 short
:
#include <stdio.h>
int main( void )
{
int x;
short y;
printf( "Gimme an x: " );
scanf( "%d", &x );
y = 2 * x + 30;
printf( "x = %d, y = %hd\n", x, y );
return 0;
}
我使用 -Wa,-aldh
选项和 gcc
来生成汇编代码列表,源代码交错,给我
GAS LISTING /tmp/cc3D25hf.s page 1
1 .file "simple.c"
2 .text
3 .Ltext0:
4 .section .rodata
5 .LC0:
6 0000 47696D6D .string "Gimme an x: "
6 6520616E
6 20783A20
6 00
7 .LC1:
8 000d 256400 .string "%d"
9 .LC2:
10 0010 78203D20 .string "x = %d, y = %hd\n"
10 25642C20
10 79203D20
10 2568640A
10 00
11 .text
12 .globl main
14 main:
15 .LFB0:
16 .file 1 "simple.c"
1:simple.c **** #include <stdio.h>
2:simple.c ****
3:simple.c **** int main( void )
4:simple.c **** {
17 .loc 1 4 0
18 .cfi_startproc
19 0000 55 pushq %rbp
20 .cfi_def_cfa_offset 16
21 .cfi_offset 6, -16
22 0001 4889E5 movq %rsp, %rbp
23 .cfi_def_cfa_register 6
24 0004 4883EC10 subq , %rsp
5:simple.c **** int x;
6:simple.c **** short y;
7:simple.c ****
8:simple.c **** printf( "Gimme an x: " );
25 .loc 1 8 0
26 0008 BF000000 movl $.LC0, %edi
26 00
27 000d B8000000 movl [=11=], %eax
27 00
28 0012 E8000000 call printf
28 00
9:simple.c **** scanf( "%d", &x );
29 .loc 1 9 0
30 0017 488D45F8 leaq -8(%rbp), %rax
31 001b 4889C6 movq %rax, %rsi
32 001e BF000000 movl $.LC1, %edi
32 00
33 0023 B8000000 movl [=11=], %eax
33 00
34 0028 E8000000 call __isoc99_scanf
34 00
10:simple.c ****
11:simple.c **** y = 2 * x + 30;
GAS LISTING /tmp/cc3D25hf.s page 2
35 .loc 1 11 0
36 002d 8B45F8 movl -8(%rbp), %eax
37 0030 83C00F addl , %eax
38 0033 01C0 addl %eax, %eax
39 0035 668945FE movw %ax, -2(%rbp)
12:simple.c ****
13:simple.c **** printf( "x = %d, y = %hd\n", x, y );
40 .loc 1 13 0
41 0039 0FBF55FE movswl -2(%rbp), %edx
42 003d 8B45F8 movl -8(%rbp), %eax
43 0040 89C6 movl %eax, %esi
44 0042 BF000000 movl $.LC2, %edi
44 00
45 0047 B8000000 movl [=11=], %eax
45 00
46 004c E8000000 call printf
46 00
14:simple.c **** return 0;
47 .loc 1 14 0
48 0051 B8000000 movl [=11=], %eax
48 00
15:simple.c **** }
49 .loc 1 15 0
50 0056 C9 leave
51 .cfi_def_cfa 7, 8
52 0057 C3 ret
53 .cfi_endproc
54 .LFE0:
56 .Letext0:
57 .file 2 "/usr/lib/gcc/x86_64-redhat-linux/7/include/stddef.h"
58 .file 3 "/usr/include/bits/types.h"
59 .file 4 "/usr/include/libio.h"
60 .file 5 "/usr/include/stdio.h"
如果你看线条
36 002d 8B45F8 movl -8(%rbp), %eax
37 0030 83C00F addl , %eax
38 0033 01C0 addl %eax, %eax
39 0035 668945FE movw %ax, -2(%rbp)
这是
的机器代码y = 2 * x + 30;
当它处理 x
时,它使用长字的操作码:
movl -8(%rbp), %eax ;; copy the value in x to the eax register
addl , %eax ;; add the literal value 15 to the value in eax
addl %eax, %eax ;; multiply the value in eax by 2
当它处理 y
时,它使用单词的操作码:
movw %ax, -2(%rbp) ;; save the value in the lower 2 bytes of eax to y
这就是它“知道”要为给定对象读取多少字节的方式 - 所有这些信息都被写入机器代码本身。标量类型都有固定的已知大小,因此只需选择正确的操作码或使用操作码即可。
- 我使用的是 Intel 特定的助记符,但其他汇编器的概念是相同的。
TL;DR 程序员知道他们想要做什么,并在编程语言中通过变量和表达式表达出来and/or 语句。编译器生成的机器代码告诉处理器要做什么才能执行该程序。程序员通知程序(通过编写程序);程序通知编译器,编译器通知机器代码(通过翻译程序),机器代码在动态执行机器代码期间通知处理器。
高级编程语言根据变量以及操作变量的表达式或语句进行操作。变量是一种逻辑结构——变量通常在程序执行时创建和销毁。然而,处理器根据在存储上运行的机器代码指令来运行。存储是一种物理构造 — 存储只是一直存在,它不会被创建或销毁1,而是会被重复使用或改变用途。
此外,处理器不会读取这样的变量声明——机器代码中没有真正等同于变量声明的变量声明。因为处理器只读取机器代码指令,所以每条机器代码指令都必须通过机器代码指令告诉处理器它需要知道的关于用于变量的存储的所有信息。通过最少的错误检查,处理器简单地相信机器代码程序对存储做了一些有用的事情。
编程语言编译器的工作是将逻辑变量声明映射到物理存储保留中,并将表达式和语句翻译成机器代码指令,这些指令可以操纵保存程序变量的物理存储。
编译器读取并记住所有变量声明,因此当一行程序代码操作变量时,编译器知道要使用什么物理存储(通过查询其逻辑到物理映射)以及什么类型或用于该机器代码翻译的大小。对于赋值运算,通常真正需要的只是大小,但对于其他算术运算,还需要其他属性,例如signed-ness(signed vs. unsigned)或数字类型(integer vs. floating point)。
编译器理解程序,因为它是用定义明确的语言规范编写的,因此它可以读取变量声明和代码语句并将它们翻译成机器代码。由无错误编译器生成的机器代码始终以正确的方式使用给定变量的物理存储,给定程序的变量声明和语言规则的定义。这意味着编译器将使用机器代码指令进行翻译,这些机器代码指令按照程序员编写该程序的意图来操作物理存储。
因此,如果程序员将变量指定为 2 字节宽,编译器将识别(至少)2 字节的物理存储,并在该变量的生命周期内具有适当的持续时间,并且每当程序操作该变量时,编译器将生成机器代码来访问它用适当的大小和其他属性标识的物理存储。
堆栈只是一个内存区域,其使用几乎直接对应于 1:1 函数调用和调用链或调用堆栈。因此,它是放置生命周期限于函数持续时间的变量的好地方。因为函数的存储空间在其 return 时被释放,该物理存储空间可以被另一个新调用的函数重用。
堆栈内存上的操作(大部分)是使用与所有其他类型的内存相同的机器代码指令完成的——这些机器代码指令根据需要通知处理器大小和类型——只是堆栈是软件已知的由函数调用重新调整用途并由函数 return 释放。处理器对堆栈知之甚少。
(一些处理器有专门的 push
和 pop
指令以堆栈指针寄存器引用的内存为目标,这可以使执行这些操作比其他方式更有效)。
1(模虚拟内存但那是另一回事;计算机中的实际 RAM 和 CPU 寄存器都预先&post-在物理量不变的情况下存在程序)。