C 在内存中(引擎盖下)创建结构时的操作顺序是什么?
假设我有一个简单的C系统:C 在内存中(引擎盖下)创建结构时的操作顺序是什么?,c,assembly,x86-64,C,Assembly,X86 64,假设我有一个简单的C系统: #include <cstddef> typedef struct Point { Point *a; Point *b; int x; int y; } Point; int main() { Point p1 = {NULL, NULL, 3, 5}; return 0; } 再向前迈出一小步,我们有: int main() { Point v = {NULL, NULL, 3, 5}; Point m
#include <cstddef>
typedef struct Point {
Point *a;
Point *b;
int x;
int y;
} Point;
int main() {
Point p1 = {NULL, NULL, 3, 5};
return 0;
}
再向前迈出一小步,我们有:
int main() {
Point v = {NULL, NULL, 3, 5};
Point m = {NULL, NULL, 7, 9};
Point s = {&v, &s, 11, 12};
return 0;
}
汇编至:
main:
push rbp ; save the base pointer to the stack.
mov rbp, rsp ; put the previous stack pointer into the base pointer.
mov QWORD PTR [rbp-32], 0
mov QWORD PTR [rbp-24], 0
mov DWORD PTR [rbp-16], 3
mov DWORD PTR [rbp-12], 5
mov QWORD PTR [rbp-64], 0
mov QWORD PTR [rbp-56], 0
mov DWORD PTR [rbp-48], 7
mov DWORD PTR [rbp-44], 9
mov QWORD PTR [rbp-96], 0
mov QWORD PTR [rbp-88], 0
mov QWORD PTR [rbp-80], 0
mov DWORD PTR [rbp-80], 11
mov DWORD PTR [rbp-76], 12
lea rax, [rbp-32]
mov QWORD PTR [rbp-96], rax
lea rax, [rbp-96]
mov QWORD PTR [rbp-88], rax
mov eax, 0
pop rbp
ret
我还不能确切地说出发生了什么,但是(有一点)。你能解释一下最后一个例子中发生了什么吗?我不太明白基指针是什么,我知道堆栈指针是什么。我不知道是什么,但它说的是四字大小和指针/地址。但为什么它要从rbp中选择这些特定的偏移量呢?我不明白为什么它会选择这个
第二部分是本文的重点。它看起来像是在处理我所做的部分{&v,&s}
所以我的问题是:
a(b(c(),d(),e(f(g(),h(),…))
。首先计算最深的函数,然后在参数中传递a
最后计算。但我很难想象这在组装中会是什么样子
更具体地说,我试图在汇编中创建一个简单的键/值存储,以便更深入地理解如何在这个低级别创建“对象”。使用JavaScript很容易:
db[key] = value
但这是因为value
已经存在于内存中的某个地方。我的问题是,我是否应该直接在关键值存储中预先创建它?或者您总是在内存中的一个随机空闲点创建它(比如从rbp
的偏移量),然后将它们移动到正确的位置(或者将它们指向正确的位置)?我一直认为我应该直接在分支上创建树叶节点,就像我在分支上粘贴一片叶子一样(视觉上)。但是叶子已经存在了!它在分支上之前存在于何处!?在其他地方构建之前,它能存在于分支上吗?我弄糊涂了
所以,从一片叶子开始
Most compiler use the stack for local variables.
Space on the stack is usually managed by two pointers: The stack pointer; and a "base" pointer that points to the base of the "allocated" memory on the stack.
Also worth to note is that the stack on almost all systems grows downward, which is why there are negative offsets from the base pointer (register rbp
in your generated code).
The amount of space reserved is calculated by the compiler, which add code to initialize the two pointers either inside the function or before the function is called (it depends on calling conventions).
When the function returns the pointers are reset, which is a very simple way to "free" the memory for the local variables.
Somewhat illustrated, it looks like this:
base pointer ---> +---------------------+
| Space for variables |
| ... |
| ... |
| ... |
stack pointer --> +---------------------+
大多数编译器对局部变量使用堆栈
堆栈上的空间通常由两个指针管理:堆栈指针;以及一个“基”指针,指向堆栈上“已分配”内存的基
另外值得注意的是,几乎所有系统上的堆栈都向下增长,这就是为什么在生成的代码中存在与基指针的负偏移(registerrbp
)
保留的空间量由编译器计算,编译器在函数内部或调用函数之前(取决于调用约定)添加代码来初始化两个指针
当函数返回时,指针被重置,这是为局部变量“释放”内存的一种非常简单的方法
从某种程度上讲,它看起来是这样的:
main:
push rbp ; save the base pointer to the stack.
mov rbp, rsp ; put the previous stack pointer into the base pointer.
mov QWORD PTR [rbp-32], 0 ; Write 0 (NULL) to v.a
mov QWORD PTR [rbp-24], 0 ; Write 0 (NULL) to v.b
mov DWORD PTR [rbp-16], 3 ; Write 3 to v.x
mov DWORD PTR [rbp-12], 5 ; Write 5 to v.y
mov QWORD PTR [rbp-64], 0 ; Write 0 (NULL) m.a
mov QWORD PTR [rbp-56], 0 ; Write 0 (NULL) to m.b
mov DWORD PTR [rbp-48], 7 ; Write 7 to m.x
mov DWORD PTR [rbp-44], 9 ; Write 9 to m.y
mov QWORD PTR [rbp-96], 0 ; Write 0 (NULL) to s.a
mov QWORD PTR [rbp-88], 0 ; Write 0 (NULL) to s.b
mov QWORD PTR [rbp-80], 0 ; Write 0 to s.x
mov DWORD PTR [rbp-80], 11 ; Write 11 to s.x
mov DWORD PTR [rbp-76], 12 ; Write 11 to s.y
lea rax, [rbp-32] ; Load effective address of v.a into rax
mov QWORD PTR [rbp-96], rax ; Write address of v.a into s.a
lea rax, [rbp-96] ; Load effective address of s.a into rax
mov QWORD PTR [rbp-88], rax ; Write address of m.a into s.b
mov eax, 0
pop rbp
ret
基本指针-->+---------------------+
|变量空间|
| ... |
| ... |
| ... |
堆栈指针-->+---------------------+
在函数中(通常),参数和局部变量被组织到堆栈帧中(以及前一帧的地址和下一条指令的地址),并通过与基(或帧)指针的偏移量进行引用rbp
存储堆栈帧的地址,通过偏移该地址来引用对象。为什么不直接从堆栈指针(rsp
)偏移呢?根据您在函数中执行的操作,堆栈指针可能会发生变化(在编译代码中变化不大,在手工破解的程序集中变化更大)。基准或帧指针为进行偏移提供了一个稳定、不变的参考点。那又怎样
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 10, 14 sdk_version 10, 14
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
xorl %eax, %eax
movl $0, -4(%rbp)
movq l___const.main.v(%rip), %rcx
movq %rcx, -32(%rbp)
movq l___const.main.v+8(%rip), %rcx
movq %rcx, -24(%rbp)
movq l___const.main.v+16(%rip), %rcx
movq %rcx, -16(%rbp)
movq l___const.main.s(%rip), %rcx
movq %rcx, -56(%rbp)
movq l___const.main.s+8(%rip), %rcx
movq %rcx, -48(%rbp)
movq l___const.main.s+16(%rip), %rcx
movq %rcx, -40(%rbp)
leaq -32(%rbp), %rcx
movq %rcx, -80(%rbp)
leaq -56(%rbp), %rcx
movq %rcx, -72(%rbp)
movl $11, -64(%rbp)
movl $12, -60(%rbp)
popq %rbp
retq
.cfi_endproc
## -- End function
.section __TEXT,__const
.p2align 3 ## @__const.main.v
l___const.main.v:
.quad 0
.quad 0
.long 3 ## 0x3
.long 5 ## 0x5
.p2align 3 ## @__const.main.s
l___const.main.s:
.quad 0
.quad 0
.long 7 ## 0x7
.long 9 ## 0x9
.subsections_via_symbols
意思是“将扩展为QWORD(8字节)的立即数操作数0的值写入从rbp-32
计算的地址”。如果rbp
为0xdeadbeef
,则表示从0xdeadbeef-0x20
或0xdeadbecf
开始的8个字节归零
生成的代码中有一些奇怪之处-不确定为什么在写入11
之前将s.x
归零。也不知道为什么在复制m
和s
的地址之前,它会费劲地将s.a
和s
归零(一个struct
对象的地址和它的第一个成员的地址总是相同的)。启用优化可能会解决这个问题
编译器就是这样做的。不同的编译器可能会执行不同的操作—例如,这是Mac上gcc(LLVM)的输出:
int *bad_return_local() {
int buf[100]; // on the stack; destroyed when the function returns
return buf; // caller can't use this pointer to out-of-scope automatic storage
}
int *good_return_dynamic() {
int *buf = malloc(100*sizeof(*buf)); // on "the heap"
if (!buf) /* error: couldn't allocate memory */;
return buf; // caller must manually free() the return value at some point
}
int *return_static() {
static int buf[100]; // static storage, e.g. in the BSS, same as global scope
return buf; // return the same pointer to the same storage every call
}
不同的语法,不同的方法,相同的结果
基本上,我想知道直接在堆上而不是在堆栈上创建东西是什么样子的
在C中不能有一个命名变量“在堆上”。只能有指向动态分配存储的指针。(指针变量本身是本地或全局、自动或静态存储,但它所持有的值可以是指向malloc
返回值的指针)
e、 g.int*buffer=malloc(100*sizeof(*buffer))函数内部的代码:buffer
是一个局部变量(自动存储,这意味着堆栈空间或只是主流ISA上“正常”C实现的寄存器)
*buffer
是该动态存储块的第一个int
有些托管语言不像C语言那样区分动态存储和自动存储。e、 g.在C#或Java中,您始终可以
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 10, 14 sdk_version 10, 14
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
xorl %eax, %eax
movl $0, -4(%rbp)
movq l___const.main.v(%rip), %rcx
movq %rcx, -32(%rbp)
movq l___const.main.v+8(%rip), %rcx
movq %rcx, -24(%rbp)
movq l___const.main.v+16(%rip), %rcx
movq %rcx, -16(%rbp)
movq l___const.main.s(%rip), %rcx
movq %rcx, -56(%rbp)
movq l___const.main.s+8(%rip), %rcx
movq %rcx, -48(%rbp)
movq l___const.main.s+16(%rip), %rcx
movq %rcx, -40(%rbp)
leaq -32(%rbp), %rcx
movq %rcx, -80(%rbp)
leaq -56(%rbp), %rcx
movq %rcx, -72(%rbp)
movl $11, -64(%rbp)
movl $12, -60(%rbp)
popq %rbp
retq
.cfi_endproc
## -- End function
.section __TEXT,__const
.p2align 3 ## @__const.main.v
l___const.main.v:
.quad 0
.quad 0
.long 3 ## 0x3
.long 5 ## 0x5
.p2align 3 ## @__const.main.s
l___const.main.s:
.quad 0
.quad 0
.long 7 ## 0x7
.long 9 ## 0x9
.subsections_via_symbols
int *bad_return_local() {
int buf[100]; // on the stack; destroyed when the function returns
return buf; // caller can't use this pointer to out-of-scope automatic storage
}
int *good_return_dynamic() {
int *buf = malloc(100*sizeof(*buf)); // on "the heap"
if (!buf) /* error: couldn't allocate memory */;
return buf; // caller must manually free() the return value at some point
}
int *return_static() {
static int buf[100]; // static storage, e.g. in the BSS, same as global scope
return buf; // return the same pointer to the same storage every call
}