V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
yezheyu
V2EX  ›  程序员

请教一个很基础的变量内存分配问题

  •  1
     
  •   yezheyu · 2021-08-01 18:26:29 +08:00 · 4049 次点击
    这是一个创建于 991 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我声明一个整型变量 a = 10

    会在栈空间开辟一块内存来存 10 这个值,假设地址是 0x002

    这也就意味着变量 a 的值的地址是 0x002,那么我想问下变量 a 本身存在哪?

    程序是怎么知道 0x002 这个地址在命名空间中叫 a,总该有个地方存 a 吧?

    是在栈区起始位置以类似 a:0x002 这种把变量名和其值的地址放在一起存起来吗?还是其它什么机制?

    最近看了很多博客,都没提到这点,有老哥帮忙解释下吗?

    44 条回复    2021-08-03 17:33:04 +08:00
    zoharSoul
        1
    zoharSoul  
       2021-08-01 18:31:35 +08:00
    首先不一定是栈,
    java 的可能在堆
    felixin
        2
    felixin  
       2021-08-01 18:52:45 +08:00 via Android   ❤️ 4
    汇编里没有变量的概念,所有用到 a 的地方都已经替换成了实际地址
    ccde8259
        3
    ccde8259  
       2021-08-01 18:55:28 +08:00 via iPhone
    栈帧里的本地变量表?
    songlinliee
        4
    songlinliee  
       2021-08-01 18:59:04 +08:00
    我的理解,在底层操作的时候会直接对地址操作,编译器会把变量转换成虚拟地址,然后通过地址转换器分配物理地址。
    编译器负责根据软件逻辑来分配虚拟地址,MMU 负责管理实际物理内存。
    wwhontheway
        5
    wwhontheway  
       2021-08-01 19:10:30 +08:00 via Android
    编译的时候都变成地址了吧
    wangxn
        6
    wangxn  
       2021-08-01 19:10:39 +08:00
    变量名和实际地址的对应关系只有在编译时才是必要的。运行时直接操作地址就行。
    chashao
        7
    chashao  
       2021-08-01 19:13:14 +08:00 via iPhone
    编译的时候用到这个栈变量地址的地方都会变成栈指针偏移来表示吧
    yezheyu
        8
    yezheyu  
    OP
       2021-08-01 19:15:48 +08:00
    @wangxn 也就会说变量名只存在于代码中给程序员看的,编译后已经被替换成内存地址 0x002,内存中实际存在的是 0x002,而 a 根本就没存在过,这样理解对吗?
    wangxn
        9
    wangxn  
       2021-08-01 19:45:33 +08:00
    @yezheyu 是的。可以这么理解。不过一般调试版本的可执行文件还是会附带这个对应关系用来调试,生产环境用的发布版本就不必要了。
    akira
        10
    akira  
       2021-08-01 20:04:37 +08:00
    @yezheyu 编译型的语言,是的。 其实你只要去看一下对应的汇编语言,基本上就很清晰了
    mainlong
        11
    mainlong  
       2021-08-01 20:08:46 +08:00
    程序运行需要环境,比如运行 python 就需要 python 解释器,那么解释器就会针对这段代码产生 temp 文件,里面就有变量 a 和地址 0x002 的映射。


    我猜大概就是这个意思。肯定有编译器,解释器的参与。因为硬件不认识变量 a 。
    hahasong
        12
    hahasong  
       2021-08-01 21:15:04 +08:00 via iPhone
    编译过程中会有个符号表
    chenyu8674
        13
    chenyu8674  
       2021-08-01 21:21:34 +08:00   ❤️ 2
    建议看下《编译原理》,之前不懂的很多问题都能想明白

    另外通过(尤其是中文环境下的)博客学习知识是效率很低的做法
    Mohanson
        14
    Mohanson  
       2021-08-01 21:43:47 +08:00
    事实上 a=10 这玩意大概率是放在指令的立即数里面的
    littlewing
        15
    littlewing  
       2021-08-01 22:54:54 +08:00
    符号表
    xiadong1994
        16
    xiadong1994  
       2021-08-01 22:58:36 +08:00 via iPhone
    如果是一段很小的代码,优化后 a 可能直接放寄存器,内存地址都没有
    ipwx
        17
    ipwx  
       2021-08-01 23:33:58 +08:00
    +1 可能在寄存器里面
    hefish
        18
    hefish  
       2021-08-01 23:41:23 +08:00
    同学,你应该去看一门课程,叫编译原理
    GeruzoniAnsasu
        19
    GeruzoniAnsasu  
       2021-08-01 23:52:01 +08:00   ❤️ 4
    有一个“学编程”学不到的很重要的事实:

    编译器并不「翻译你写的代码」,它实际上是「照着你的代码创造能产生一致结果的过程」。


    在不同语言里,“声明 a=10”很可能会产生完全不一样的结果。
    - C 系语言在未优化前可能会分配一块代表 a 的内存,内存的位置取决于 a 所在的作用域;
    - 一些解释型语言比如 cpython 会创造一个存放 10 的对象,这会给对象本身开一块内存,然后在特定结构中放 10,甚至一个指向静态常量表里「 10 」的指针;
    - 一些语言完全没有“变量”,只有“名字绑定”,所以大概会在特定符号表结构中插入「 a 」以及它的替换物「 10 」


    所以#8

    > 也就会说变量名只存在于代码中给程序员看的,编译后已经被替换成内存地址 0x002,内存中实际存在的是 0x002,而 a 根本就没存在过,这样理解对吗

    仅仅是特定情况下(指 C 系语言中)可能(指没被优化掉,变量正常地参与了计算等等)是对的
    msg7086
        20
    msg7086  
       2021-08-02 04:48:39 +08:00 via Android
    另外,编译器很可能会把你写的代码替换成等效的别的代码。比如你写一个循环,把输入的数字累加 100 次,编译器很可能把这个操作转写成乘法,编译的时候甚至可能编译成位移加法。经过重度优化后的程序可能已经跟你写的代码完全不同了。
    happinessnch
        21
    happinessnch  
       2021-08-02 09:35:13 +08:00
    - a 本身存在哪?
    a 只是一个代号,不需要存储,无论是解释型还是编译型,a 作为一个代号,在解释或者编译的过程中,就会履行代号的职责,替换成代替的内容。

    - a 程序怎么知道 0x002 叫 a ?
    a 为什么是 0x002,这个是“程序”分配给 a 的,a 是给人读的,不是给“程序”读的,对于程序员来说 a 的可读性比 0x002 好的多,是方便程序写出规模更大的程序所需要的。

    - 其值的地址放在一起存起来吗?
    前面说了 a 对于“程序”来说无关紧要,根本不需要存,而 0x002 则是操作系统的虚拟地址空间里的地址,虚拟地址会映射到实际的物理内存上。
    lackywind
        22
    lackywind  
       2021-08-02 09:40:13 +08:00
    直接给你上一段 C++的反汇编吧,看了应该就明白了。。。
    ![]( https://i.bmp.ovh/imgs/2021/08/55d10f51f1327bd9.png)
    nuk
        23
    nuk  
       2021-08-02 09:46:37 +08:00
    每个 function 都有自己的 local symbol,但是仅限栈地址,如果寄存器传值就看不到了。
    nuk
        24
    nuk  
       2021-08-02 09:58:03 +08:00
    用 readelf --dump-debug 就可以看到,这里我定义两个局部变量 x,y,调试信息里就有这两个变量相对栈指针的偏移量。
    <2><4e>: Abbrev Number: 3 (DW_TAG_variable)
    <4f> DW_AT_name : x
    <51> DW_AT_decl_file : 1
    <52> DW_AT_decl_line : 3
    <53> DW_AT_type : <0x67>
    <57> DW_AT_location : 2 byte block: 91 5c (DW_OP_fbreg: -36)
    <2><5a>: Abbrev Number: 3 (DW_TAG_variable)
    <5b> DW_AT_name : y
    <5d> DW_AT_decl_file : 1
    <5e> DW_AT_decl_line : 4
    <5f> DW_AT_type : <0x6e>
    <63> DW_AT_location : 2 byte block: 91 60 (DW_OP_fbreg: -32)
    weiwenhao
        25
    weiwenhao  
       2021-08-02 10:22:17 +08:00
    不考虑寄存器分配的情况下, 你在函数定义了
    int a = 1
    int b = 2
    int c = 3

    实际上就是。
    mov 1 => EBP+4 字节
    mov 2 => EBP+8 字节
    mov 3 => EBP+12 字节

    在编译阶段就已经确定了,a = EBP + 4 , b = EBP + 8....了

    gcc -s main.c 可以看到
    iOCZ
        26
    iOCZ  
       2021-08-02 10:52:49 +08:00
    其实栈底和栈顶限制了栈的区域,所谓的本地变量的地址也只是汇编中写死的栈顶-offset 值而已
    mxT52CRuqR6o5
        27
    mxT52CRuqR6o5  
       2021-08-02 11:15:30 +08:00
    就比如 ebp-0x0004 这个地址就是 a 的地址,所有用到 a 的地方就都直接用 ebp-0x0004,汇编层面就没有变量 a 这个概念了
    icyalala
        28
    icyalala  
       2021-08-02 11:43:36 +08:00
    一般来说,在未优化的编译下,这种变量是放在函数的栈帧 (Stack Frame) 里的。
    但实际编译优化后,根据你的使用方法,甚至可以被优化为一条 lea 指令,所以不要在这里纠结了。
    多试试: https://godbolt.org/z/5v9MW6d78
    netwjx
        29
    netwjx  
       2021-08-02 12:01:45 +08:00
    楼主需要补课了 编译原理
    Kasumi20
        30
    Kasumi20  
       2021-08-02 12:06:50 +08:00
    这是魔怔了,建议学一下汇编语言
    dangyuluo
        31
    dangyuluo  
       2021-08-02 12:39:26 +08:00
    你是想知道编译器(链接器)是怎么知道 a 这个符号对应哪个内存地址的?用 objdump 看看 ELF 的符号表就知道了
    Feep
        32
    Feep  
       2021-08-02 12:42:01 +08:00 via iPhone
    1.变量 a 就存在以地址 0x002 为起始地址的一片内存上,这片空间的大小由声明 /定义的变量类型决定。
    2.计算机执行程序不需要知道变量的名字,只需要对象的地址。编译器会把变量名和地址操作翻译成汇编指令,实际上经过编译后的汇编代码已经没有变量名信息了。
    3.不知道怎么解释了……因为内存的栈区不存变量名。
    发现有什么不对的地方请老哥们帮忙纠正。
    FrankFang128
        33
    FrankFang128  
       2021-08-02 12:56:38 +08:00
    看一下《编译原理》比较好,a 在 AST 里存在,但最后执行的时候可能有 a,也可能没有。
    zxCoder
        34
    zxCoder  
       2021-08-02 15:20:09 +08:00
    反对楼上动不动就让人看编译原理的。。。。

    这玩意跟编译原理没啥关系,这个问题在初学 c/c++这种语言的时候很常见

    特别是如果先学了 java,思想更难掰过来,不过仔细思考一下应该就能理解了
    3dwelcome
        35
    3dwelcome  
       2021-08-02 16:03:36 +08:00
    我怎么感觉,局部变量都是每一次进入函数体后,动态赋值的呢。

    也就程序编译后,有一个只读数据区,专门存不变的数值,比如楼主的 10 。

    运行时,程序把 10 从只读数据区取出来,动态复制到 0x002 这个位置。而 0x002 这种地址,每次调用函数都会变,gcc 是通过_alloc 函数,动态分配内存的(通过修改 ESP 偏移)。
    3dwelcome
        36
    3dwelcome  
       2021-08-02 16:14:31 +08:00
    试了 28 楼的在线编译器,怎么和我本地的 clang 结果不一样呢?

    我的只要函数体内局部变量稍大,有栈大小渗出风险,都会智能通过调用_alloc 来二次优化栈空间分配函数。

    而在线编译器就是暴力偏移 stack pointer,哪怕局部变量大的离谱,明显运行结果会导致栈渗出。
    libook
        37
    libook  
       2021-08-02 17:08:46 +08:00
    我觉得这个就是编译原理的知识范畴,我自己也没有完整学过编译原理,但是之前学 Rust 的时候为了理解 stack 和 heap 的作用专门去挑了编译原理对应的章节去学习,你可以看 B 站的这个公开课,只看 9.2 章节应该就能解决你的疑惑了 https://www.bilibili.com/video/BV11t411V74n?p=41

    简单来讲就是编译完成后,机器码中可能就已经用 0x002 这个地址来替代 a 这个标识符了。
    然后每个作用域都有个基地址,0x002 是在这个基地址基础上的相对地址,这样你不同作用域下的 a 实际上就在不同的地址上。
    再然后值可能存在 stack 上,也可能存在 heap 上,这个取决于你目前用的编译器的设计,一种简单的设计是:stack 中 0x002 这个相对地址上存储的值是 heap 上的相对起始地址、长度,然后在 heap 的这个区域里存储 0x002 的值,可以很方便地修改;当然如果这是个常量,那么 stack 上 0x002 这个相对地址可能就直接存的值。
    kop1989
        38
    kop1989  
       2021-08-02 18:02:28 +08:00   ❤️ 1
    这个 a,是你起的名字,也是为了给你看的。
    对于计算机而言,他不需要知道 a 叫什么。

    编译的时候已经把“变量 a 等于 10”这个逻辑替换成了“栈内存第零位地址是 0xxx”(这个 0xxx 对应的是常量 10 ),任何相同类型的 10 都是这个地址。

    所以你后续的比如 a++,就会翻译为“栈内存的第 0 位的地址从 0xxx 改为 0xxy”。
    kop1989
        39
    kop1989  
       2021-08-02 18:08:29 +08:00   ❤️ 1
    换句话说,相同的变量的名字,其实体现在程序里就是一个相同的位置。
    这个位置未见得一定在栈。

    举个尴尬的例子,你点外卖的时候,你会选择地址为“公司”、“家庭”等等 tag 。
    但对于外卖小哥而言,他分辨的是你住在 xx 区 xx 栋。至于说你管这个叫“公司”还是“家”,对他没有意义。而且这个 tag 对最终的外卖送达也没有任何影响。

    程序也是如此,变量名只是一个 tag,并不影响最终的程序逻辑。
    xylxAdai
        40
    xylxAdai  
       2021-08-02 18:15:30 +08:00
    一般在汇编里面直接就变成相对地址了。
    在 debug 版本存在符号表的可执行文件中,可能还存在了 a 这个符号,这个符号一般在符号表内,可以自己看看这个可执行文件的内容
    monstervivi
        41
    monstervivi  
       2021-08-02 18:20:00 +08:00
    推荐你看一下 CSAPP 的第三章,看完你就懂啦
    ryd994
        42
    ryd994  
       2021-08-02 23:10:38 +08:00 via Android
    编译原理是一方面。另一方面你可以学一下汇编。写过一个小汇编程序的话你就知道自己的程序编译后是什么样子的了。
    进阶还可以看看自己写的程序的反编译。然后发现编译器这都什么怪物?优化的比我写的汇编好多了
    rpman
        43
    rpman  
       2021-08-03 11:53:23 +08:00
    学编译原理大可不必,学下汇编就懂了
    amok
        44
    amok  
       2021-08-03 17:33:04 +08:00
    变量的值存在栈帧的局部变量表里,类似数组的形式。编译后的字节码指令,并不存在变量名 a,只是通过索引对变量表的值进行操作。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1118 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 23:01 · PVG 07:01 · LAX 16:01 · JFK 19:01
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.