流程部分:声明是否也向 .text 添加了一些内容?如果是,它添加了什么?
Process sections: does a declaration add also something to .text? If yes, what does it add?
我有一个像这样的 C 代码,它可能会被编译到 ARM 的 ELF 文件中:
int a;
int b=1;
int foo(int x) {
int c=2;
static float d=1.5;
// .......
}
我知道所有可执行代码都进入 .text
部分,而 .data
、 .bss
和 .rodata
将包含各种 variables/constants。
我的问题是:像 int b=1;
这样的行是否也向 .text
部分添加了一些内容,或者它只是告诉编译器将一个初始化为 1 的新变量放入 .data
(然后可能部署在最终硬件上时映射到 RAM 内存中)?
此外,在尝试反编译类似的代码时,我注意到函数 foo()
中的一行 int c=2;
向 stack
添加了一些内容,但也添加了一些内容.text
行,其中实际存储了值“2”。
那么,一般来说,声明是否总是暗示在程序集级别添加到 .text
的内容?如果是,是否取决于上下文(即变量是否在函数内部,是否是局部全局变量,...)以及实际添加的内容?
非常感谢。
正如@goodvibration 正确指出的那样,只有全局或静态变量进入段。这是因为它们的生命周期就是程序的整个执行时间。
局部变量有不同的生命周期。它们仅在它们定义的块(例如函数)执行期间存在。如果调用一个函数,所有不适合寄存器的参数都会被压入堆栈,并且 return 地址被写入 link 寄存器。* 该函数可能会保存 link 寄存器和堆栈中的其他寄存器 并在堆栈中为局部 变量添加一些 space (这是您观察到的代码)。在函数结束时,弹出保存的寄存器并重新调整堆栈指针。通过这种方式,您可以获得局部变量的自动垃圾收集。
*:请注意,这仅适用于 ARM(的某些调用约定)。这是不同的,例如适用于英特尔处理器。
does a line like int b=1;
here add also something to the .text section, or does it only tell the compiler to place a new variable initialized to 1 in .data (then probably mapped in RAM memory when deployed on the final hardware)?
您了解这可能是特定于实现的,但很可能您只会在数据部分获得初始化数据。如果它是一个常量,它可能会进入文本部分。
Moreover, trying to decompile a similar code, I noticed that a line such as int c=2;
, inside the function foo()
, was adding something to the stack, but also some lines of .text where the value '2' was actually memorized there.
初始化的自动变量,每次进入函数的范围时都必须初始化。 c
的 space 保留在堆栈上(或在寄存器中,取决于 ABI),但程序必须记住初始化它的常量,最好将其放在文本中的某个位置段,作为常量值或作为 "move immediate" 指令。
So, in general, does a declaration always imply also something added to .text at an assembly level?
没有。如果静态变量被初始化为零或 null 或根本没有初始化,通常只需要在 bss 中保留 space 即可。如果静态非常量变量被初始化为非零值,它只会被放入数据段。
这是其中之一。
int a;
int b=1;
int foo(int x) {
int c=2;
static float d=1.5;
int e;
e=x+2;
return(e);
}
第一件事没有优化。
arm-none-eabi-gcc -c so.c -o so.o
arm-none-eabi-objdump -D so.o
arm-none-eabi-ld -Ttext=0x1000 -Tdata=0x2000 so.o -o so.elf
arm-none-eabi-ld: warning: cannot find entry symbol _start; defaulting to 0000000000001000
arm-none-eabi-objdump -D so.elf > so.list
不要担心警告,需要 link 才能看到一切都找到了归宿
Disassembly of section .text:
00001000 <foo>:
1000: e52db004 push {r11} ; (str r11, [sp, #-4]!)
1004: e28db000 add r11, sp, #0
1008: e24dd014 sub sp, sp, #20
100c: e50b0010 str r0, [r11, #-16]
1010: e3a03002 mov r3, #2
1014: e50b3008 str r3, [r11, #-8]
1018: e51b3010 ldr r3, [r11, #-16]
101c: e2833002 add r3, r3, #2
1020: e50b300c str r3, [r11, #-12]
1024: e51b300c ldr r3, [r11, #-12]
1028: e1a00003 mov r0, r3
102c: e28bd000 add sp, r11, #0
1030: e49db004 pop {r11} ; (ldr r11, [sp], #4)
1034: e12fff1e bx lr
Disassembly of section .data:
00002000 <b>:
2000: 00000001 andeq r0, r0, r1
00002004 <d.4102>:
2004: 3fc00000 svccc 0x00c00000
Disassembly of section .bss:
00002008 <a>:
2008: 00000000 andeq r0, r0, r0
作为反汇编,它会尝试反汇编数据,因此忽略它(例如 0x2008 旁边的 andeq)。
a 变量是全局的且未初始化的,因此它位于 .bss 中(通常......编译器可以选择做任何它想做的事情,只要它正确地实现了语言,就不必有一个叫做 .bss 的东西例如,但 gnu 和许多其他人都这样做)。
b 是全局的并已初始化,因此它位于 .data 中,如果它被声明为 const,它可能会位于 .rodata 中,具体取决于编译器及其提供的内容。
c 是一个被初始化的本地非静态变量,因为 C 提供递归这需要在堆栈上(或使用寄存器或其他易失性资源管理),并初始化每个 运行。我们需要在没有优化的情况下编译才能看到这个
1010: e3a03002 mov r3, #2
1014: e50b3008 str r3, [r11, #-8]
d 是我所说的局部全局变量,它是一个静态局部变量,因此它存在于函数之外,而不是在堆栈中,与全局变量一起存在,但只能进行局部访问。
我在你的示例中添加了e,这是一个本地未初始化,但随后使用的。如果我没有使用它并且没有优化,可能会为它分配 space 但没有初始化。
将 x 保存在堆栈中(根据此调用约定 x 进入 r0)
100c: e50b0010 str r0, [r11, #-16]
然后从栈中加载x,将两个相加,保存为栈中的e。从
此调用约定的堆栈和位置 return 位置是 r0.
1018: e51b3010 ldr r3, [r11, #-16]
101c: e2833002 add r3, r3, #2
1020: e50b300c str r3, [r11, #-12]
1024: e51b300c ldr r3, [r11, #-12]
1028: e1a00003 mov r0, r3
对于所有架构,未优化这有点典型,总是从堆栈中读取变量并快速将它们放回原处。其他架构对于传入参数和传出 return 值所在的位置有不同的调用约定。
如果我优化(gcc 行上的-O2)
Disassembly of section .text:
00001000 <foo>:
1000: e2800002 add r0, r0, #2
1004: e12fff1e bx lr
Disassembly of section .data:
00002000 <b>:
2000: 00000001 andeq r0, r0, r1
Disassembly of section .bss:
00002004 <a>:
2004: 00000000 andeq r0, r0, r0
b 是全局的,所以在对象级别必须为它保留全局 space,它是 .data,优化不会改变它。
a 也是全局的并且仍然是 .bss,因为在对象级别它被声明为这样分配以防另一个对象需要它。 linker 没有删除这些。
现在 c 和 d 是死代码,它们什么都不做,不需要存储,所以
c 不再分配 space 在堆栈上, d 也不再分配任何 .data
space.
对于此代码的此调用约定,我们有大量用于此体系结构的寄存器,因此 e 不需要在
堆栈,它进入 r0 数学可以用 r0 完成,然后它是 returned in r0.
我知道我没有告诉 linker 将 .bss 放在哪里,而是告诉它 .data 它毫无怨言地将 .bss 放在同一个 space 中。例如,我可以放 -Tbss=0x3000 来给它自己的 space 或者只是完成一个 linker 脚本。链接器脚本会对典型结果造成严重破坏,因此请当心。
典型,但可能有例外的编译器:
非常量全局变量进入 .data 或 .bss 取决于它们是否在声明期间被初始化。
如果 const 那么也许 .rodata 或 .text 取决于(或者 .data 或 .bss 在技术上可行)
非静态局部变量根据需要进入通用寄存器或堆栈(如果未完全优化)。
静态局部变量(如果没有优化掉)与全局变量一起生活但不能全局访问它们只是像全局变量一样在 .data 或 .bss 中分配 space。
参数完全由该编译器为该目标使用的调用约定控制。仅仅因为 arm 或 mips 或其他可能已经写下约定并不意味着编译器必须使用它,只有当他们声称支持某些约定或标准时他们才应该尝试遵守。为了使编译器有用,它需要一个约定并坚持它,无论它是什么,以便函数的调用者和被调用者都知道从哪里获取参数和 return 一个值。具有足够寄存器的架构通常会有一个约定,其中少数几个寄存器用于前这么多参数(不一定是一对一),然后堆栈用于所有其他参数。同样,如果可能,可以使用寄存器来获取 return 值。由于缺少 gprs 或其他原因,某些体系结构在两个方向上都使用堆栈。或一个堆栈和另一个寄存器。欢迎您找出约定并尝试阅读它们,但归根结底,您使用的编译器(如果没有被破坏)会遵循约定,并且通过设置像上面那样的实验,您可以看到约定在起作用。
加上在这种情况下的优化。
void more_fun ( unsigned long long );
unsigned fun ( unsigned int x, unsigned long long y )
{
more_fun(y);
return(x+1);
}
如果我告诉你 arm 约定通常使用 r0-r3 作为前几个参数,你可能会假设 x 在 r0 中,r1 和 r2 用于 y,我们可以在需要堆栈之前有另一个小参数,出色地
也许是旧的手臂,但现在它希望 64 位变量使用偶数然后使用奇数。
00000000 <fun>:
0: e92d4010 push {r4, lr}
4: e1a04000 mov r4, r0
8: e1a01003 mov r1, r3
c: e1a00002 mov r0, r2
10: ebfffffe bl 0 <more_fun>
14: e2840001 add r0, r4, #1
18: e8bd4010 pop {r4, lr}
1c: e12fff1e bx lr
所以 r0 包含 x,r2/r3 包含 y 并且 r1 被忽略了。
测试被精心设计为没有 y 作为死代码并将其传递给另一个函数,我们可以看到 y 在进入 fun 和离开 more_fun 的过程中的存储位置。 r2/r3在来的路上,需要在r0/r1才能叫的更开心
我们需要为 return 保留 x 的乐趣。人们可能期望 x 会落在堆栈上,但它会未经优化,而是保存一个寄存器,约定已声明将由函数 (r4) 保留,并在整个函数中或至少在该函数中使用 r4 来存储 x。性能优化,如果 x 需要被多次访问,则进入堆栈的内存周期比寄存器访问成本更高。
然后它计算 return 并清理堆栈、寄存器。
IMO 重要的是要看到这一点,调用约定对某些变量起作用,而其他变量可能会根据优化而有所不同,没有优化它们是大多数人会立即声明的,.bss,.data( .text/.rodata),然后进行优化,这取决于变量是否存在。
我有一个像这样的 C 代码,它可能会被编译到 ARM 的 ELF 文件中:
int a;
int b=1;
int foo(int x) {
int c=2;
static float d=1.5;
// .......
}
我知道所有可执行代码都进入 .text
部分,而 .data
、 .bss
和 .rodata
将包含各种 variables/constants。
我的问题是:像 int b=1;
这样的行是否也向 .text
部分添加了一些内容,或者它只是告诉编译器将一个初始化为 1 的新变量放入 .data
(然后可能部署在最终硬件上时映射到 RAM 内存中)?
此外,在尝试反编译类似的代码时,我注意到函数 foo()
中的一行 int c=2;
向 stack
添加了一些内容,但也添加了一些内容.text
行,其中实际存储了值“2”。
那么,一般来说,声明是否总是暗示在程序集级别添加到 .text
的内容?如果是,是否取决于上下文(即变量是否在函数内部,是否是局部全局变量,...)以及实际添加的内容?
非常感谢。
正如@goodvibration 正确指出的那样,只有全局或静态变量进入段。这是因为它们的生命周期就是程序的整个执行时间。
局部变量有不同的生命周期。它们仅在它们定义的块(例如函数)执行期间存在。如果调用一个函数,所有不适合寄存器的参数都会被压入堆栈,并且 return 地址被写入 link 寄存器。* 该函数可能会保存 link 寄存器和堆栈中的其他寄存器 并在堆栈中为局部 变量添加一些 space (这是您观察到的代码)。在函数结束时,弹出保存的寄存器并重新调整堆栈指针。通过这种方式,您可以获得局部变量的自动垃圾收集。
*:请注意,这仅适用于 ARM(的某些调用约定)。这是不同的,例如适用于英特尔处理器。
does a line like
int b=1;
here add also something to the .text section, or does it only tell the compiler to place a new variable initialized to 1 in .data (then probably mapped in RAM memory when deployed on the final hardware)?
您了解这可能是特定于实现的,但很可能您只会在数据部分获得初始化数据。如果它是一个常量,它可能会进入文本部分。
Moreover, trying to decompile a similar code, I noticed that a line such as
int c=2;
, inside the functionfoo()
, was adding something to the stack, but also some lines of .text where the value '2' was actually memorized there.
初始化的自动变量,每次进入函数的范围时都必须初始化。 c
的 space 保留在堆栈上(或在寄存器中,取决于 ABI),但程序必须记住初始化它的常量,最好将其放在文本中的某个位置段,作为常量值或作为 "move immediate" 指令。
So, in general, does a declaration always imply also something added to .text at an assembly level?
没有。如果静态变量被初始化为零或 null 或根本没有初始化,通常只需要在 bss 中保留 space 即可。如果静态非常量变量被初始化为非零值,它只会被放入数据段。
这是其中之一。
int a;
int b=1;
int foo(int x) {
int c=2;
static float d=1.5;
int e;
e=x+2;
return(e);
}
第一件事没有优化。
arm-none-eabi-gcc -c so.c -o so.o
arm-none-eabi-objdump -D so.o
arm-none-eabi-ld -Ttext=0x1000 -Tdata=0x2000 so.o -o so.elf
arm-none-eabi-ld: warning: cannot find entry symbol _start; defaulting to 0000000000001000
arm-none-eabi-objdump -D so.elf > so.list
不要担心警告,需要 link 才能看到一切都找到了归宿
Disassembly of section .text:
00001000 <foo>:
1000: e52db004 push {r11} ; (str r11, [sp, #-4]!)
1004: e28db000 add r11, sp, #0
1008: e24dd014 sub sp, sp, #20
100c: e50b0010 str r0, [r11, #-16]
1010: e3a03002 mov r3, #2
1014: e50b3008 str r3, [r11, #-8]
1018: e51b3010 ldr r3, [r11, #-16]
101c: e2833002 add r3, r3, #2
1020: e50b300c str r3, [r11, #-12]
1024: e51b300c ldr r3, [r11, #-12]
1028: e1a00003 mov r0, r3
102c: e28bd000 add sp, r11, #0
1030: e49db004 pop {r11} ; (ldr r11, [sp], #4)
1034: e12fff1e bx lr
Disassembly of section .data:
00002000 <b>:
2000: 00000001 andeq r0, r0, r1
00002004 <d.4102>:
2004: 3fc00000 svccc 0x00c00000
Disassembly of section .bss:
00002008 <a>:
2008: 00000000 andeq r0, r0, r0
作为反汇编,它会尝试反汇编数据,因此忽略它(例如 0x2008 旁边的 andeq)。
a 变量是全局的且未初始化的,因此它位于 .bss 中(通常......编译器可以选择做任何它想做的事情,只要它正确地实现了语言,就不必有一个叫做 .bss 的东西例如,但 gnu 和许多其他人都这样做)。
b 是全局的并已初始化,因此它位于 .data 中,如果它被声明为 const,它可能会位于 .rodata 中,具体取决于编译器及其提供的内容。
c 是一个被初始化的本地非静态变量,因为 C 提供递归这需要在堆栈上(或使用寄存器或其他易失性资源管理),并初始化每个 运行。我们需要在没有优化的情况下编译才能看到这个
1010: e3a03002 mov r3, #2
1014: e50b3008 str r3, [r11, #-8]
d 是我所说的局部全局变量,它是一个静态局部变量,因此它存在于函数之外,而不是在堆栈中,与全局变量一起存在,但只能进行局部访问。
我在你的示例中添加了e,这是一个本地未初始化,但随后使用的。如果我没有使用它并且没有优化,可能会为它分配 space 但没有初始化。
将 x 保存在堆栈中(根据此调用约定 x 进入 r0)
100c: e50b0010 str r0, [r11, #-16]
然后从栈中加载x,将两个相加,保存为栈中的e。从 此调用约定的堆栈和位置 return 位置是 r0.
1018: e51b3010 ldr r3, [r11, #-16]
101c: e2833002 add r3, r3, #2
1020: e50b300c str r3, [r11, #-12]
1024: e51b300c ldr r3, [r11, #-12]
1028: e1a00003 mov r0, r3
对于所有架构,未优化这有点典型,总是从堆栈中读取变量并快速将它们放回原处。其他架构对于传入参数和传出 return 值所在的位置有不同的调用约定。
如果我优化(gcc 行上的-O2)
Disassembly of section .text:
00001000 <foo>:
1000: e2800002 add r0, r0, #2
1004: e12fff1e bx lr
Disassembly of section .data:
00002000 <b>:
2000: 00000001 andeq r0, r0, r1
Disassembly of section .bss:
00002004 <a>:
2004: 00000000 andeq r0, r0, r0
b 是全局的,所以在对象级别必须为它保留全局 space,它是 .data,优化不会改变它。
a 也是全局的并且仍然是 .bss,因为在对象级别它被声明为这样分配以防另一个对象需要它。 linker 没有删除这些。
现在 c 和 d 是死代码,它们什么都不做,不需要存储,所以 c 不再分配 space 在堆栈上, d 也不再分配任何 .data space.
对于此代码的此调用约定,我们有大量用于此体系结构的寄存器,因此 e 不需要在 堆栈,它进入 r0 数学可以用 r0 完成,然后它是 returned in r0.
我知道我没有告诉 linker 将 .bss 放在哪里,而是告诉它 .data 它毫无怨言地将 .bss 放在同一个 space 中。例如,我可以放 -Tbss=0x3000 来给它自己的 space 或者只是完成一个 linker 脚本。链接器脚本会对典型结果造成严重破坏,因此请当心。
典型,但可能有例外的编译器:
非常量全局变量进入 .data 或 .bss 取决于它们是否在声明期间被初始化。 如果 const 那么也许 .rodata 或 .text 取决于(或者 .data 或 .bss 在技术上可行)
非静态局部变量根据需要进入通用寄存器或堆栈(如果未完全优化)。
静态局部变量(如果没有优化掉)与全局变量一起生活但不能全局访问它们只是像全局变量一样在 .data 或 .bss 中分配 space。
参数完全由该编译器为该目标使用的调用约定控制。仅仅因为 arm 或 mips 或其他可能已经写下约定并不意味着编译器必须使用它,只有当他们声称支持某些约定或标准时他们才应该尝试遵守。为了使编译器有用,它需要一个约定并坚持它,无论它是什么,以便函数的调用者和被调用者都知道从哪里获取参数和 return 一个值。具有足够寄存器的架构通常会有一个约定,其中少数几个寄存器用于前这么多参数(不一定是一对一),然后堆栈用于所有其他参数。同样,如果可能,可以使用寄存器来获取 return 值。由于缺少 gprs 或其他原因,某些体系结构在两个方向上都使用堆栈。或一个堆栈和另一个寄存器。欢迎您找出约定并尝试阅读它们,但归根结底,您使用的编译器(如果没有被破坏)会遵循约定,并且通过设置像上面那样的实验,您可以看到约定在起作用。
加上在这种情况下的优化。
void more_fun ( unsigned long long );
unsigned fun ( unsigned int x, unsigned long long y )
{
more_fun(y);
return(x+1);
}
如果我告诉你 arm 约定通常使用 r0-r3 作为前几个参数,你可能会假设 x 在 r0 中,r1 和 r2 用于 y,我们可以在需要堆栈之前有另一个小参数,出色地 也许是旧的手臂,但现在它希望 64 位变量使用偶数然后使用奇数。
00000000 <fun>:
0: e92d4010 push {r4, lr}
4: e1a04000 mov r4, r0
8: e1a01003 mov r1, r3
c: e1a00002 mov r0, r2
10: ebfffffe bl 0 <more_fun>
14: e2840001 add r0, r4, #1
18: e8bd4010 pop {r4, lr}
1c: e12fff1e bx lr
所以 r0 包含 x,r2/r3 包含 y 并且 r1 被忽略了。
测试被精心设计为没有 y 作为死代码并将其传递给另一个函数,我们可以看到 y 在进入 fun 和离开 more_fun 的过程中的存储位置。 r2/r3在来的路上,需要在r0/r1才能叫的更开心
我们需要为 return 保留 x 的乐趣。人们可能期望 x 会落在堆栈上,但它会未经优化,而是保存一个寄存器,约定已声明将由函数 (r4) 保留,并在整个函数中或至少在该函数中使用 r4 来存储 x。性能优化,如果 x 需要被多次访问,则进入堆栈的内存周期比寄存器访问成本更高。
然后它计算 return 并清理堆栈、寄存器。
IMO 重要的是要看到这一点,调用约定对某些变量起作用,而其他变量可能会根据优化而有所不同,没有优化它们是大多数人会立即声明的,.bss,.data( .text/.rodata),然后进行优化,这取决于变量是否存在。