【问题标题】:How does the stack differentiate between different number types?堆栈如何区分不同的数字类型?
【发布时间】:2021-10-30 08:43:46
【问题描述】:

我正在尝试学习汇编,但在理解堆栈上的内存分配/检索时遇到了一些麻烦。

在堆栈上分配字符串时,程序知道在到达空终止字符/x00 时停止读取字符串。然而,有了数字,就没有这样的事情了。程序如何知道分配在堆栈上的数字的结尾,以及它如何区分不同的数字类型(短、长、整数)? (我对此有点陌生,所以请纠正我可能误解的任何内容!)

【问题讨论】:

  • 每个数字类型都有固定的大小。类型声明用于知道为其分配多少空间以及使用哪些指令来读取/写入它。
  • 处理器不知道,堆栈也不知道。由程序员来跟踪这一点。
  • 这与栈内存无关;在那里分配/释放空间的机制与 malloc/free 或 mmap/munmap 或 BSS / .data / .rodata 中的静态存储不同,但仍然只是作为堆栈帧一部分的字节块。

标签: c assembly types integer stack-memory


【解决方案1】:

类型int vs. float vs. char * vs. struct foo)只有在编译器分析您的源代码并将其转换为合适的机器码。这就是“[] 的操作数之一应具有指针类型,另一个应具有整数类型”和“一元 * 的操作数应具有指针类型”和“乘法运算符的操作数应具有算术类型”之类的规则"等,都被强制执行了。

汇编语言通常处理字节、字(2 个字节)、长字(4 个字节)等,尽管一些特殊用途的平台可能有奇怪的字长。操作码addb1 添加两个字节大小的实体的内容,addl 添加两个长字大小的实体的内容,等等。所以当编译器翻译你的代码时,它使用基于其声明类型的对象的正确操作码。因此,如果您将某些内容声明为short,编译器将(通常)使用用于字大小对象的操作码(addwmovw 等)。如果您将某些内容声明为intlong,它将(通常)使用用于长字大小对象的操作码(addlmovl)。浮点类型通常使用一组不同的操作码和它们自己的一组寄存器来处理。

简而言之,汇编语言通过编译器指定的操作码“知道”事物的位置和大小。

简单示例 - 这是一些与 intshort 一起使用的 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    $16, %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    $0, %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    $0, %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    $15, %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    $0, %eax
  45      00
  46 004c E8000000      call    printf
  46      00
  14:simple.c      ****   return 0;
  47                    .loc 1 14 0
  48 0051 B8000000      movl    $0, %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    $15, %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    $15, %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

这就是它“知道”为给定对象读取多少字节的方式 - 所有这些信息都被烘焙到机器代码本身中。标量类型都有固定的已知大小,因此只需选择正确的操作码或要使用的操作码即可。


  1. 我使用的是 Intel 特定的助记符,但其他汇编程序的概念是相同的。

【讨论】:

    【解决方案2】:

    TL;DR 程序员知道他们想要做什么,并在编程语言中以变量、表达式和/或语句的形式表达出来。编译器生成的机器代码告诉处理器为了执行该程序要做什么。程序员通知程序(通过编写程序);程序通知编译器,编译器通知机器代码(通过翻译程序),它在机器代码的动态执行期间通知处理器。

    高级编程语言根据变量以及操作变量的表达式或语句进行操作。变量是一种逻辑结构——变量通常在程序执行时创建和销毁。然而,处理器根据在存储上运行的机器代码指令进行操作。存储是一种物理构造 - 存储始终存在,它不会被创建或销毁1,而是被重复使用或重新利用。

    此外,处理器不会像这样读取变量声明——机器代码中没有真正等同于变量声明的东西。因为处理器只读取机器代码指令,所以每条机器代码指令都必须通过机器代码指令告诉处理器它需要知道的关于用于变量的存储的所有信息。通过最少的错误检查,处理器只相信机器代码程序对存储做了一些有用的事情。

    编程语言编译器的工作是将逻辑变量声明映射到物理存储预留中,并将表达式和语句转换为机器代码指令,以操纵保存程序变量的物理存储。

    编译器读取并记住所有变量声明,因此当一行程序代码操作变量时,编译器知道要使用什么物理存储(通过查询其逻辑到物理映射)以及要使用什么类型或大小用于该机器代码翻译。对于赋值运算,通常只需要大小,但对于其他算术运算,还需要其他属性,例如有符号(有符号与无符号)或数字类型(整数与浮点)。

    编译器理解程序,因为它是用定义良好的语言规范编写的,因此它可以读取变量声明和代码语句并将它们翻译成机器代码。给定程序的变量声明和语言规则的定义,由无错误编译器生成的机器代码始终以正确的方式使用给定变量的物理存储。这意味着编译器将使用机器代码指令进行翻译,这些指令按照程序员编写程序时的预期方式操作物理存储。

    因此,如果程序员将变量指定为 2 字节宽,编译器将识别(至少)2 字节物理存储,并在该变量的生命周期内具有适当的持续时间,并且每当程序操作该变量时,编译器将生成机器代码来访问它所标识的具有适当大小和其他属性的物理存储。

    堆栈只是一个内存区域,其使用几乎直接 1:1 对应于函数调用和调用链或调用堆栈。因此,它是放置生命周期受限于函数持续时间的变量的好地方。因为函数的存储在其返回时被释放,所以该物理存储会被另一个新调用的函数重用。

    堆栈内存上的操作(大部分)使用与所有其他类型内存相同的机器代码指令完成 - 这些机器代码指令根据需要通知处理器大小和类型 - 只是软件知道堆栈是由函数调用改变用途并由函数返回释放。处理器对堆栈知之甚少。

    (某些处理器具有专用的pushpop 指令,这些指令针对堆栈指针寄存器引用的内存,这可以使执行这些操作比其他方式更有效)。


    1(取模虚拟内存,但这是另一回事;计算机中的实际 RAM 和 CPU 寄存器在程序之前和之后都存在,而物理量没有变化)。子>

    【讨论】:

      【解决方案3】:

      程序如何知道分配在堆栈上的数字的结尾,以及它如何区分不同的数字类型(short、long、int)?

      绝对知道。也就是说,它无法通过查看那里的字节来确定驻留在堆栈(或内存中其他地方)的任何内容的类型。相反,程序必须知道在什么位置期望什么数据类型,并执行适合这些类型的指令。在某些情况下,它甚至可能会根据不同的数据类型来解释相同的数据。

      【讨论】:

      • 这就是我要问的——程序如何知道何时停止读取字节?它怎么知道从堆栈中读取 8 个字节为 long,一个字节为 char 等?
      • @Maslin:给定平台的类型表示是编译器内置的——一些编译器开发人员编码“long = 8 bytes, char = 1 byte”等。所以当你写@987654323 @ 编译器记录了 llong 类型的事实,并在堆栈上为其分配 8 个字节,然后当您编写 l=17; 时,它因此知道发出 8 字节存储指令。
      • @Maslin:你看过 C 编译器的 asm 输出吗? (如godbolt.org 并参见How to remove "noise" from GCC/clang assembly output?)。它使用具有固定大小的指令,具体大小取决于您使用的 C 类型,因此操作数大小嵌入到机器代码中。数据只是您可以访问的一袋字节,编译器创建程序的 asm 版本,以有用的方式访问它。编辑:或查看约翰博德对这个问题的回答!
      • @Maslin,程序“知道”,因为编译器插入机器指令以从正确的位置读取一个或多个适当宽度的值。访问大小在每次访问时直接构建到程序中。在这个级别没有数据类型,只有机器指令和它们的参数,它们一起非常直接地指示如何移动数据和操作它。
      猜你喜欢
      • 2011-03-04
      • 1970-01-01
      • 2014-05-09
      • 2014-07-22
      • 1970-01-01
      • 2019-09-28
      • 1970-01-01
      • 2012-11-02
      • 2012-11-05
      相关资源
      最近更新 更多