1. 背景知識
1.1 Linux進程、線程的內存布局
在各種有棧協(xié)程的實現(xiàn)中,不論是獨立協(xié)程棧還是共享棧,都依托于線程棧的基礎,而線程又共享使用進程的地址空間。
為了真正理解協(xié)程棧以及在協(xié)程切換時的棧保存以及恢復過程,首先需要徹底理解進程的地址空間以及線程棧如何被管理和使用。
這里不討論進程與線程如何的異同,關注重點在于內存地址空間如何分配使用。
1.1.1 進程的地址空間
對于Linux 64位系統(tǒng),理論上,64bit內存地址可用空間為0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF(16位十六進制數(shù)),這是個相當龐大的空間,Linux實際上只用了其中一小部分(256T)。
X86_64架構4級頁表下(注意在5級頁表下虛擬內存地址空間會更加龐大),實際用到的地址空間為0x0000000000000000 ~ 0x00007FFFFFFFFFFF(user space)和0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF(kernel space),其余的都是未被使用的空洞。
也就是說,在64位的巨大地址空間中,僅使用了低地址的128TB用作用戶虛擬內存空間,高地址的128TB用作內核虛擬內存空間。就像一個夾心巨厚的三明治,只吃掉了上下兩層面包,而中間的夾心扔掉了。
| 開始地址 | 結束地址 | 空間大小 | 用途 |
|---|---|---|---|
| 0x0000000000000000 | 0x00007FFFFFFFFFFF | 128TB | 用戶態(tài)虛擬內存空間,每個進程獨立 |
| 0x0000800000000000 | 0xFFFF7FFFFFFFFFFF | 約16M TB | 巨大的空洞,未使用空間 |
| 0xFFFF800000000000 | 0xFFFFFFFFFFFFFFFF | 128TB | 內核態(tài)虛擬內存空間,所有進程共享 |
128TB的內核態(tài)虛擬內存空間
劃分大致如下:
8TB虛擬化預留空間、64TB直接物理內存映射空間(page_offset_base)、32TB vmalloc/ioremap空間、1TB虛擬內存映射空間、16TB KASAN 鏡像空間、以及一些GB或MB單位的小型空間
由此可見,64位Linux能支持的最大物理內存為64TB。-
128TB的用戶態(tài)虛擬內存空間
借用一張32位的分布圖,沒有找到合適的64位的圖
image.png
可見從低地址向高地址,依次有主要的幾個Segment:
- Text Segment:代碼段,加載保存進程的二進制程序
- Data Segment:數(shù)據(jù)段, 保存被初始化的靜態(tài)局部變量或被初始化的全局變量
- BSS Segment: 保存未被初始化的靜態(tài)局部變量或未被初始化的全局變量
- Heap: 堆空間, 由brk分配的內存,地址向上增長
- Memory Mapping Segment: 由mmap分配的內存,地址向下增長,與Heap的增長方向相反,二者向中間靠攏。二者在編程時通常都是調用庫函數(shù)malloc分配出來的。文件映射,包括動態(tài)庫文件的映射,也在此段空間。
- Stack: ??臻g,從高地址向低地址增長,Stack Size受限于系統(tǒng)RLIMIT_STACK,默認為8MB。
下面用一個簡單的程序來觀察進程的內存分布
test.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static char *sv_inited1 = "hello"; //用于觀察數(shù)據(jù)段Data Segment
static char *sv_inited2 = "world"; //用于觀察數(shù)據(jù)段Data Segment
static char *sv_uninited1; //用于觀察BSS Segment
static char *sv_uninited2; //用于觀察BSS Segment
int main() {
int v_stack1 = 1; //用于觀察棧 - stack
void *fromBrk1 = malloc(64); //用于觀察堆 - heap
void *fromBrk2 = malloc(64); //用于觀察堆 - heap
void *from_mmap1 =
malloc(4 * 1024 * 1024); //用于觀察mmap位置 - Memory Mapping
void *from_mmap2 = malloc(10 * 1024 * 1024); //用于觀察mmap位置
printf("Data Segment: sv_inited1: %p\n", &sv_inited1);
printf("Data Segment: sv_inited2: %p\n", &sv_inited2);
printf("sv_inited1 > sv_inited2 ? %d\n\n", &sv_inited1 > &sv_inited2);
printf("BSS Segment: sv_uninited1: %p\n", &sv_uninited1);
printf("BSS Segment: sv_uninited2: %p\n", &sv_uninited2);
printf("sv_uninited1 > sv_uninited2 ? %d\n\n", &sv_uninited1 > &sv_uninited2);
printf("gap between Heap and BSS: %lu \n\n",
(unsigned long)fromBrk1 - (unsigned long)&sv_uninited2);
printf("Heap: fromBrk1: %p\n", fromBrk1);
printf("Heap: fromBrk2: %p\n", fromBrk2);
printf("fromBrk1 > fromBrk2 ? %d\n\n", fromBrk1 > fromBrk2);
printf("Heap bottom to MMapping top: %lu \n\n",
(unsigned long)from_mmap1 - (unsigned long)fromBrk1);
printf("Memory Mapping: from_mmap1: %p\n", from_mmap1);
printf("Memory Mapping: from_mmap2: %p\n", from_mmap2);
printf("from_mmap1 > from_mmap2 ? %d, from_mmap1-from_mmap2=%ld\n\n",
from_mmap1 > from_mmap2, from_mmap1 - from_mmap2);
printf("gap between Memory Mapping and Stack: %lu \n\n",
(unsigned long)&v_stack1 - (unsigned long)from_mmap1);
printf("Stack: v_stack1: %p\n", &v_stack1);
getchar();
return 0;
}
使用objdump觀察一下段概要信息,省略掉一些Segment
這里只能看到text到bss段,后面的Heap、Memory Mapping、Stack只能在運行時來觀察。
可以看出,text、data、bss段的排列緊湊而且都很小,之間沒有明顯的間隙。
$ objdump -h a.out
a.out: 文件格式 elf64-x86-64
節(jié):
Idx Name Size VMA LMA File off Algn
0 .interp 0000001c 0000000000000238 0000000000000238 00000238 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
... ...
13 .text 000003c2 0000000000000670 0000000000000670 00000670 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
... ...
22 .data 00000020 0000000000202000 0000000000202000 00002000 2**3
CONTENTS, ALLOC, LOAD, DATA
23 .bss 00000018 0000000000202020 0000000000202020 00002020 2**3
ALLOC
... ...
然后結合程序輸出和pmap來觀察運行時的情況
$ pmap -x 25520
25520: ./a.out
住址 Kbytes RSS Dirty Mode Mapping
00005637323af000 4 4 0 r-x-- a.out
00005637323af000 0 0 0 r-x-- a.out
00005637325b0000 4 4 4 r---- a.out
00005637325b0000 0 0 0 r---- a.out
00005637325b1000 4 4 4 rw--- a.out
00005637325b1000 0 0 0 rw--- a.out
0000563732c23000 132 4 4 rw--- [ anon ]
0000563732c23000 0 0 0 rw--- [ anon ]
00007fdff42dc000 2052 4 4 rw--- [ anon ]
00007fdff42dc000 0 0 0 rw--- [ anon ]
00007fdff44dd000 1948 1156 0 r-x-- libc-2.27.so
00007fdff44dd000 0 0 0 r-x-- libc-2.27.so
00007fdff46c4000 2048 0 0 ----- libc-2.27.so
00007fdff46c4000 0 0 0 ----- libc-2.27.so
00007fdff48c4000 16 16 16 r---- libc-2.27.so
00007fdff48c4000 0 0 0 r---- libc-2.27.so
00007fdff48c8000 8 8 8 rw--- libc-2.27.so
00007fdff48c8000 0 0 0 rw--- libc-2.27.so
00007fdff48ca000 16 12 12 rw--- [ anon ]
00007fdff48ca000 0 0 0 rw--- [ anon ]
00007fdff48ce000 164 164 0 r-x-- ld-2.27.so
00007fdff48ce000 0 0 0 r-x-- ld-2.27.so
00007fdff49ca000 1036 12 12 rw--- [ anon ]
00007fdff49ca000 0 0 0 rw--- [ anon ]
00007fdff4af7000 4 4 4 r---- ld-2.27.so
00007fdff4af7000 0 0 0 r---- ld-2.27.so
00007fdff4af8000 4 4 4 rw--- ld-2.27.so
00007fdff4af8000 0 0 0 rw--- ld-2.27.so
00007fdff4af9000 4 4 4 rw--- [ anon ]
00007fdff4af9000 0 0 0 rw--- [ anon ]
00007ffd8fb29000 132 12 12 rw--- [ stack ]
00007ffd8fb29000 0 0 0 rw--- [ stack ]
00007ffd8fb6f000 12 0 0 r---- [ anon ]
00007ffd8fb6f000 0 0 0 r---- [ anon ]
00007ffd8fb72000 4 4 0 r-x-- [ anon ]
00007ffd8fb72000 0 0 0 r-x-- [ anon ]
ffffffffff600000 4 0 0 --x-- [ anon ]
ffffffffff600000 0 0 0 --x-- [ anon ]
---------------- ------- ------- -------
total kB 7596 1416 88
提取幾條關鍵信息:
4 4 0 r-x-- a.out # text段起始位置
132 4 4 rw--- [ anon ] #大致是Heap起始位置(低地址)
0 0 0 rw--- ld-2.27.so #大致是MMapping結束位置(高地址)
132 12 12 rw--- [ stack ] #stack高地址
再看程序輸出:
Data Segment: sv_inited1: 0x5637325b1010
Data Segment: sv_inited2: 0x5637325b1018
sv_inited1 > sv_inited2 ? 0
BSS Segment: sv_uninited1: 0x5637325b1028
BSS Segment: sv_uninited2: 0x5637325b1030
sv_uninited1 > sv_uninited2 ? 0
gap between Heap and BSS: 6758960
Heap: fromBrk1: 0x563732c23260
Heap: fromBrk2: 0x563732c232b0
fromBrk1 > fromBrk2 ? 0
Heap bottom to MMapping top: 45804783562160
Memory Mapping: from_mmap1: 0x7fdff49ca010
Memory Mapping: from_mmap2: 0x7fdff42dc010
from_mmap1 > from_mmap2 ? 1, from_mmap1-from_mmap2=7266304
gap between Memory Mapping and Stack: 127156083924
Stack: v_stack1: 0x7ffd8fb47ce4
從關鍵提取信息和程序輸出來看,二者各段地址大致相當,比較吻合。
從程序輸出來看,
Heap bottom to MMapping top: 45804783562160
這是一個非常大的空間,40多TB,這里就是malloc的發(fā)揮空間了。
gap between Memory Mapping and Stack: 127156083924
這里有接近120GB的空間,由于棧向低地址增長,所以理論上??臻g可以占用這部分,但是棧大小受到操作系統(tǒng)限制,可以通過ulimit -s來查看,單位是KB,默認是8192 ,8MB。
1.1.2 線程上下文
在多線程程序中,多個線程并發(fā)執(zhí)行,全局變量、Heap上的數(shù)據(jù)塊(指針)、文件映射等可以共享訪問,共享同一份程序二進制(代碼段)。但是每個線程有其獨立的上下文,比如相同的指令執(zhí)行路徑但各自不同的指令執(zhí)行位置或者完全不同的指令執(zhí)行路徑,不同的局部變量值,不同的狀態(tài)。那么這些東西是如何為每個線程獨立維護的呢?
建議讀一讀這篇文章,作者分析得很清晰,圖文并茂
https://www.51cto.com/article/719916.html
文章中講解了從父進程fork出子進程的過程,以及關鍵的內核數(shù)據(jù)結構struct task_struct,它在內核中代表了一個進程。
借兩張圖過來,畫得真好,就不自己造輪子了。


在我們的主題中,最為關心的是mm_struct所表示的用戶態(tài)虛擬內存空間,它在進程和線程之間,有些怎樣的共享和獨立的關系,這對于未來協(xié)程棧的建立非常重要。
看看pthread庫中創(chuàng)建一個線程的過程,它與fork一個子進程的差別在哪里呢?
pthread是在glibc中實現(xiàn)的,這里可以找到其源代碼,筆者慣用Ubuntu,所以使用了Ubuntu20.04的分支
$ git clone https://git.launchpad.net/ubuntu/+source/glibc
$ git checkout ubuntu/focal-devel
pthread_create的實現(xiàn)函數(shù)在nptl/pthread_create.c中
代碼比較長,我么盡可能摘取重要的部分,去掉那些異常分支部分,縮略后的代碼分析如下:
int
__pthread_create_2_1 (pthread_t *newthread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg)
{
STACK_VARIABLES; #void *stackaddr = NULL 用來標識棧頂位置
const struct pthread_attr *iattr = (struct pthread_attr *) attr; # 通常這個入?yún)⑹莻€NULL
struct pthread_attr default_attr;
bool free_cpuset = false;
bool c11 = (attr == ATTR_C11_THREAD);
if (iattr == NULL || c11)
{
lll_lock (__default_pthread_attr_lock, LLL_PRIVATE);
default_attr = __default_pthread_attr;
... iattr 通常是使用默認的attr ...
iattr = &default_attr;
}
struct pthread *pd = NULL;
int err = ALLOCATE_STACK (iattr, &pd);
int retval = 0;
pd->start_routine = start_routine;
pd->arg = arg;
pd->c11 = c11;
/* Copy the thread attribute flags. */
struct pthread *self = THREAD_SELF;
pd->flags = ((iattr->flags & ~(ATTR_FLAG_SCHED_SET | ATTR_FLAG_POLICY_SET))
| (self->flags & (ATTR_FLAG_SCHED_SET | ATTR_FLAG_POLICY_SET)));
/* Initialize the field for the ID of the thread which is waiting
for us. This is a self-reference in case the thread is created
detached. */
pd->joinid = iattr->flags & ATTR_FLAG_DETACHSTATE ? pd : NULL;
/* The debug events are inherited from the parent. */
pd->eventbuf = self->eventbuf;
/* Copy the parent's scheduling parameters. The flags will say what
is valid and what is not. */
pd->schedpolicy = self->schedpolicy;
pd->schedparam = self->schedparam;
/* Copy the stack guard canary. */
#ifdef THREAD_COPY_STACK_GUARD
THREAD_COPY_STACK_GUARD (pd);
#endif
/* Copy the pointer guard value. */
#ifdef THREAD_COPY_POINTER_GUARD
THREAD_COPY_POINTER_GUARD (pd);
#endif
/* Setup tcbhead. */
tls_setup_tcbhead (pd);
/* Verify the sysinfo bits were copied in allocate_stack if needed. */
#ifdef NEED_DL_SYSINFO
CHECK_THREAD_SYSINFO (pd);
#endif
/* Inform start_thread (above) about cancellation state that might
translate into inherited signal state. */
pd->parent_cancelhandling = THREAD_GETMEM (THREAD_SELF, cancelhandling);
/* Determine scheduling parameters for the thread. */
if (__builtin_expect ((iattr->flags & ATTR_FLAG_NOTINHERITSCHED) != 0, 0)
&& (iattr->flags & (ATTR_FLAG_SCHED_SET | ATTR_FLAG_POLICY_SET)) != 0)
{
/* Use the scheduling parameters the user provided. */
if (iattr->flags & ATTR_FLAG_POLICY_SET)
{
pd->schedpolicy = iattr->schedpolicy;
pd->flags |= ATTR_FLAG_POLICY_SET;
}
if (iattr->flags & ATTR_FLAG_SCHED_SET)
{
/* The values were validated in pthread_attr_setschedparam. */
pd->schedparam = iattr->schedparam;
pd->flags |= ATTR_FLAG_SCHED_SET;
}
if ((pd->flags & (ATTR_FLAG_SCHED_SET | ATTR_FLAG_POLICY_SET))
!= (ATTR_FLAG_SCHED_SET | ATTR_FLAG_POLICY_SET))
collect_default_sched (pd);
}
if (__glibc_unlikely (__nptl_nthreads == 1))
_IO_enable_locks ();
/* Pass the descriptor to the caller. */
*newthread = (pthread_t) pd;
LIBC_PROBE (pthread_create, 4, newthread, attr, start_routine, arg);
/* One more thread. We cannot have the thread do this itself, since it
might exist but not have been scheduled yet by the time we've returned
and need to check the value to behave correctly. We must do it before
creating the thread, in case it does get scheduled first and then
might mistakenly think it was the only thread. In the failure case,
we momentarily store a false value; this doesn't matter because there
is no kosher thing a signal handler interrupting us right here can do
that cares whether the thread count is correct. */
atomic_increment (&__nptl_nthreads);
/* Our local value of stopped_start and thread_ran can be accessed at
any time. The PD->stopped_start may only be accessed if we have
ownership of PD (see CONCURRENCY NOTES above). */
bool stopped_start = false; bool thread_ran = false;
/* Start the thread. */
if (__glibc_unlikely (report_thread_creation (pd)))
{
stopped_start = true;
/* We always create the thread stopped at startup so we can
notify the debugger. */
retval = create_thread (pd, iattr, &stopped_start,
STACK_VARIABLES_ARGS, &thread_ran);
if (retval == 0)
{
/* We retain ownership of PD until (a) (see CONCURRENCY NOTES
above). */
/* Assert stopped_start is true in both our local copy and the
PD copy. */
assert (stopped_start);
assert (pd->stopped_start);
/* Now fill in the information about the new thread in
the newly created thread's data structure. We cannot let
the new thread do this since we don't know whether it was
already scheduled when we send the event. */
pd->eventbuf.eventnum = TD_CREATE;
pd->eventbuf.eventdata = pd;
/* Enqueue the descriptor. */
do
pd->nextevent = __nptl_last_event;
while (atomic_compare_and_exchange_bool_acq (&__nptl_last_event,
pd, pd->nextevent)
!= 0);
/* Now call the function which signals the event. See
CONCURRENCY NOTES for the nptl_db interface comments. */
__nptl_create_event ();
}
}
else
retval = create_thread (pd, iattr, &stopped_start,
STACK_VARIABLES_ARGS, &thread_ran);
if (__glibc_unlikely (retval != 0))
{
if (thread_ran)
/* State (c) or (d) and we may not have PD ownership (see
CONCURRENCY NOTES above). We can assert that STOPPED_START
must have been true because thread creation didn't fail, but
thread attribute setting did. */
/* See bug 19511 which explains why doing nothing here is a
resource leak for a joinable thread. */
assert (stopped_start);
else
{
/* State (e) and we have ownership of PD (see CONCURRENCY
NOTES above). */
/* Oops, we lied for a second. */
atomic_decrement (&__nptl_nthreads);
/* Perhaps a thread wants to change the IDs and is waiting for this
stillborn thread. */
if (__glibc_unlikely (atomic_exchange_acq (&pd->setxid_futex, 0)
== -2))
futex_wake (&pd->setxid_futex, 1, FUTEX_PRIVATE);
/* Free the resources. */
__deallocate_stack (pd);
}
/* We have to translate error codes. */
if (retval == ENOMEM)
retval = EAGAIN;
}
else
{
/* We don't know if we have PD ownership. Once we check the local
stopped_start we'll know if we're in state (a) or (b) (see
CONCURRENCY NOTES above). */
if (stopped_start)
/* State (a), we own PD. The thread blocked on this lock either
because we're doing TD_CREATE event reporting, or for some
other reason that create_thread chose. Now let it run
free. */
lll_unlock (pd->lock, LLL_PRIVATE);
/* We now have for sure more than one thread. The main thread might
not yet have the flag set. No need to set the global variable
again if this is what we use. */
THREAD_SETMEM (THREAD_SELF, header.multiple_threads, 1);
}
out:
if (__glibc_unlikely (free_cpuset))
free (default_attr.cpuset);
return retval;
}
versioned_symbol (libpthread, __pthread_create_2_1, pthread_create, GLIBC_2_1);
1.1節(jié)參考資料
https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt
https://www.51cto.com/article/719916.html](https://www.51cto.com/article/719916.html
1.2 X86_64寄存器概要
為了理解有棧協(xié)程在上下文切換時進行的寄存器保存和恢復過程,需要掌握寄存器的基礎知識,重點理解函數(shù)調用過程中,涉及的主要寄存器的作用,參數(shù)及返回值的傳遞,以及函數(shù)棧切換過程。
寄存器的設計與硬件平臺架構緊密相關,下面以X86_64架構為例,其它的架構可能有巨大的差異。
1.2.1 16個通用寄存器
下面8個寄存器,由32位的通用寄存器擴展而來,r開頭的表示64位寄存器,e開頭等于原先32位的寄存器。
| 名稱 | 0-63位 | 0-31位 | 0-15位 | 0-7位 | 8-15位 |
|---|---|---|---|---|---|
| 棧頂指針寄存器 | rsp | esp | sp | spl | sph |
| 基址指針寄存器 | rbp | ebp | bp | bpl | bph |
| 基址寄存器 | rbx | ebx | bx | bl | bh |
| 目的變址寄存器 | rdi | edi | di | dil | dih |
| 源變址寄存器 | rsi | esi | si | sil | sih |
| 數(shù)據(jù)寄存器 | rdx | edx | dx | dl | dh |
| 計數(shù)寄存器 | rcx | ecx | cx | cl | ch |
| 累加寄存器 | rax | eax | ax | al | ah |
64位架構新增了8個通用寄存器,r8 - r15, 其32位、16位、8位寄存器分別加上d、w、b后綴
| 0-63位 | 0-31位 | 0-15位 | 0-7位 |
|---|---|---|---|
| r8 | r8d | r8w | r8b |
| r9 | r9d | r9w | r9b |
| r10 | r10d | r10w | r10b |
| r11 | r11d | r11w | r11b |
| r12 | r12d | r12w | r12b |
| r13 | r13d | r13w | r13b |
| r14 | r14d | r14w | r14b |
| r15 | r15d | r15w | r15b |
-
RSP(ESP) : stack pointer , 棧指針寄存器
rsp寄存器正常情況下,存放的是棧頂?shù)刂?,若用于其它用途,則使用完以后,應該恢復其原值。 -
RBP(EBP): base pointer,基址寄存器
rbp寄存器正常情況下,存放的是棧底地址,若用于其它用途,則使用完以后,應該恢復其原值。通常使用rbp+偏移量的形式來定位函數(shù)存放在棧中的局部變量。 - RAX(EAX): accumulator register,累加寄存器,通常用于存儲函數(shù)的返回值。它不僅可用于存儲函數(shù)返回值,也可以用于其它,只是用于存儲返回值屬于約定俗成的慣例。
下面用一段簡單的代碼來觀察RSP、RBP、RAX(EAX)寄存器是如何被使用的
C代碼如下 test.c:
#include <stdint.h>
uint64_t func1() {
return 0xFFFFFFFFFFFF; // 一個64位以內但超過32位最大值的整數(shù)
}
int main() {
int ret = (int)func1();
return ret;
}
將上面的代碼編譯成匯編代碼:
gcc -S -o test.S test.c
.file "test.c"
.text
.globl func1
.type func1, @function
func1:
.LFB0:
.cfi_startproc
pushq %rbp # 壓棧,保存rbp寄存器初值
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp # rbp寄存器值修改為rsp寄存器值
.cfi_def_cfa_register 6
movabsq $281474976710655, %rax # 將0xFFFFFFFFFFFF這個值寫入rax寄存器,用于函數(shù)返回值
# 由于這個數(shù)值超出了32位,編譯器認為應該使用rax寄存器,而不是eax來返回
popq %rbp # 出棧,恢復rbp寄存器在進入函數(shù)func1時的初始值
.cfi_def_cfa 7, 8
ret # func1返回,main函數(shù)中就可以通過讀取rax寄存器,得到func1的返回值
.cfi_endproc
.LFE0:
.size func1, .-func1
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
pushq %rbp # 壓棧,保存rbp寄存器初值
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp # rbp寄存器值修改為rsp寄存器值
.cfi_def_cfa_register 6
subq $16, %rsp # rsp寄存器減16,棧增長16字節(jié),預留了16字節(jié)空白內存,注意rbp保存了這16字節(jié)的高地址
movl $0, %eax # eax寄存器清零,因為main返回4字節(jié)的int,所以eax寄存器就足夠了,不需要rax寄存器
call func1 # 調用func1, func1中,將其64位的返回值寫入了rax寄存器,自然低32位的eax也被改寫了
movl %eax, -4(%rbp) # 讀取eax值寫入rbp地址-4的位置,這個位置就是變量ret的棧地址
movl -4(%rbp), %eax # 將變量ret地址的值,寫入eax寄存器,作為main函數(shù)返回值
leave # 關閉棧幀指令,恢復rbp和rsp寄存器,等于 movq %rbp %rsp + popq %rbp
.cfi_def_cfa 7, 8
ret # main函數(shù)返回
.cfi_endproc
.LFE1:
.size main, .-main
.ident "GCC: (Ubuntu 9.4.0-1ubuntu1~18.04) 9.4.0"
.section .note.GNU-stack,"",@progbits
回顧一下過程,
main函數(shù)開始 -> rbp壓棧保存 -> rbp賦值為rsp -> rsp下移16字節(jié)開辟棧幀 -> call func1
- func1( rbp壓棧保存 -> rbp賦值為rsp -> 返回值寫入rax -> 出?;謴蛂bp -> return)
-> eax寄存器值寫入變量ret地址 -> ret地址值寫入eax寄存器 -> 恢復rsp和rbp -> return
總結來看,在調用函數(shù)時,調用者將rsp設置為新開辟的棧幀高地址,被調用函數(shù)使用此地址作為自己的基址來設置rbp寄存器,而后使用rbp寄存器+偏移的方式來訪問函數(shù)內的局部變量,函數(shù)結束時,返回之前,需要恢復rsp和rbp寄存器,ret之后,調用者回到自己的棧幀,rsp和rbp值恢復到調用前的值。而rax或eax寄存器總是用來傳遞函數(shù)返回值,編譯器基于返回值類型決定使用32位還是64位的寄存器。
那么,[rsp, rbp) 這個地址區(qū)間,即為當前函數(shù)的棧幀。
繼續(xù)探究其它的通用寄存器
- RDI(EDI): destination index,目標變址寄存器,字符串運算時常用于目標指針,還用作函數(shù)調用時的第1個參數(shù)。
- RSI(ESI): source index,源變址寄存器,字符串運算時常應用于源指針,還用作函數(shù)調用時的第2個參數(shù)。
- RDX(EDX): data register,數(shù)據(jù)寄存器,I/O操作時提供外部設備接口的端口地址,還用作函數(shù)調用時的第3個參數(shù)。
- RCX(ECX): counter register,計數(shù)寄存器,一般用于循環(huán)計數(shù),還用作函數(shù)調用時的第4個參數(shù)。
-
RBX(EBX): base register,基址寄存器,主要用于存儲內存中數(shù)據(jù)存放的基礎位置 ,之后只需要知道偏移地址就可以知道內存實際地址。
將之前的text.c的func1改為多參數(shù)調用,觀察各寄存器如何被使用:
#include <stdint.h>
uint64_t func1(uint64_t u1, unsigned u2, unsigned *p3, uint64_t u4, unsigned u5,
unsigned u6, unsigned u7) {
return u1 + u2 + *p3 + u4 + u5 + u6 + u7;
}
int main() {
uint64_t a[7] = {1, 2, 3, 4, 5, 6, 7};
int ret = (int)func1(a[0], a[1], (unsigned *)&a[2], a[3], a[4], a[5], a[6]);
return ret;
}
對應的匯編分析
.file "test.c"
.text
.globl func1
.type func1, @function
func1:
.LFB0:
.cfi_startproc
pushq %rbp # rbp入棧保存
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp # 設置rbp
.cfi_def_cfa_register 6
movq %rdi, -8(%rbp) # 讀取參數(shù)1到臨時變量
movl %esi, -12(%rbp) # 讀取參數(shù)2到臨時變量
movq %rdx, -24(%rbp) # 讀取參數(shù)3到臨時變量
movq %rcx, -32(%rbp) # 讀取參數(shù)4到臨時變量
movl %r8d, -16(%rbp) # 讀取參數(shù)5到臨時變量
movl %r9d, -36(%rbp) # 讀取參數(shù)6到臨時變量
movl -12(%rbp), %edx
movq -8(%rbp), %rax
addq %rax, %rdx # 在rdx中累加參數(shù)1和2
movq -24(%rbp), %rax
movl (%rax), %eax
movl %eax, %eax
addq %rax, %rdx # 在rdx中累加參數(shù)3
movq -32(%rbp), %rax
addq %rax, %rdx # 在rdx中累加參數(shù)4
movl -16(%rbp), %eax
addq %rax, %rdx # 在rdx中累加參數(shù)5
movl -36(%rbp), %eax
addq %rax, %rdx # 在rdx中累加參數(shù)6
movl 16(%rbp), %eax # 注意這里對應參數(shù)7,是通過棧地址傳遞進來的,對應的地址是main棧幀中的參數(shù)7的地址
addq %rdx, %rax # 在rax中累加參數(shù)7,這里不繼續(xù)在rdx累加,可以省略rdx向rax再拷貝一次的過程,rax中直接就是返回值了
popq %rbp #恢復rbp,由于rsp并未改變所以不需要恢復
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size func1, .-func1
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $80, %rsp
movq %fs:40, %rax
movq %rax, -8(%rbp)
xorl %eax, %eax
movq $1, -64(%rbp) # a[0] = 1
movq $2, -56(%rbp) # a[1] = 2
movq $3, -48(%rbp) # a[2] = 3
movq $4, -40(%rbp) # a[3] = 4
movq $5, -32(%rbp) # a[4] = 5
movq $6, -24(%rbp) # a[5] = 6
movq $7, -16(%rbp) # a[6] = 7
movq -16(%rbp), %rax
movl %eax, %r8d
movq -24(%rbp), %rax
movl %eax, %r9d # 設置參數(shù)6
movq -32(%rbp), %rax
movl %eax, %r10d
movq -40(%rbp), %rdx
movq -56(%rbp), %rax
movl %eax, %edi
movq -64(%rbp), %rax
leaq -64(%rbp), %rcx
leaq 16(%rcx), %rsi
pushq %r8
movl %r10d, %r8d # 設置參數(shù)5
movq %rdx, %rcx # 設置參數(shù)4
movq %rsi, %rdx # 設置參數(shù)3
movl %edi, %esi # 設置參數(shù)2
movq %rax, %rdi # 設置參數(shù)1
call func1
addq $8, %rsp
movl %eax, -68(%rbp) # func1返回值在eax中,讀取到ret變量地址
movl -68(%rbp), %eax # 準備main函數(shù)返回值
movq -8(%rbp), %rdx
xorq %fs:40, %rdx
je .L5
call __stack_chk_fail@PLT
.L5:
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size main, .-main
.ident "GCC: (Ubuntu 9.4.0-1ubuntu1~18.04) 9.4.0"
.section .note.GNU-stack,"",@progbits
可見,函數(shù)調用時,前4個參數(shù)分別使用rdi(edi)、rsi(esi)、rdx(edx)、rcx(ecx)傳遞,第5、6參數(shù)用r8(r8d)和r9(r9d)寄存器傳遞,后續(xù)再多余的參數(shù),被調用函數(shù)會使用rbp加偏移的方式來訪問調用者函數(shù)的棧幀中的地址。
rax和rdx都被用于過累加過程,最后func1通過eax寄存器傳遞返回值。
1.2.2 6個段寄存器
段寄存器被用于內存分段尋址,即通過段基址+段內偏移段方式來尋址。那么段基址就是由這些段寄存器進行保存的。
這里借用大佬的一張圖來說明:

CS(code segment): 代碼段地址寄存器,存放代碼段的起始地址
DS(data segment):數(shù)據(jù)段地址寄存器,存放數(shù)據(jù)段的起始地址
SS(stack segment):堆棧段地址寄存器,存放堆棧段的起始地址
ES(extra segment):附加段地址寄存器,存放附加段的起始地址
32位架構新增了兩個段寄存器:
FS:數(shù)據(jù)段地址寄存器
GS:數(shù)據(jù)段地址寄存器
分段尋址過程比較復雜,需要參閱專門的文獻。這些寄存器中也未必是直接存放內存的段基址,但從這些寄存器出發(fā),最終可以訪問到想要的內存地址。
1.2.3 標志寄存器
標志寄存器:里面有眾多標記,每一位代表一個標記,記錄了 CPU 執(zhí)行指令過程中的一系列狀態(tài),這些標志大都由 CPU 自動設置和修改,了解即可,仍然借用大佬的一張圖

1.2.4 指令寄存器
RIP(EIP)寄存器,它指向了下一條要執(zhí)行的指令所存放的地址(代碼段中指令的偏移地址),CPU的工作其實就是不斷取出它指向的指令,然后執(zhí)行這條指令,同時指令寄存器繼續(xù)指向下面一條指令,如此不斷重復。
RIP寄存器比較特別,它不能在程序中顯示的讀取、修改,但它會被jmp、call和ret等指令隱式的修改,它一直在改變,永遠指向下一條指令。
1.2.5 其它寄存器
- 控制寄存器:32位 CPU 總共有cr0-cr4共5個控制寄存器,64位增加了 cr8。他們各自有不同的功能,但都存儲了 CPU 工作時的重要信息。
-
浮點寄存器:x64 處理器還提供幾組浮點寄存器,八個 80 位 x87 寄存器,八個 64 位 MMX 寄存器,原始的 8 個 128 位 SSE 寄存器集增加到 16 個。
前四個浮點參數(shù)在前四個 SSE 寄存器 xmm0-xmm3 中傳遞,浮點返回值以 xmm0 返回 - 調試寄存器:用于支持軟件調試的寄存器,用于調試器設置硬件斷點。
1.2節(jié)參考資料
http://www.itdecent.cn/p/57128e477efb - [猿佑] [寄存器]
https://baijiahao.baidu.com/s?id=1681576659524219730&wfr=spider&for=pc - [軒轅之風O][一口氣看完45個寄存器]
https://www.codenong.com/cs109543793 [GCC的內嵌匯編語法]
https://learn.microsoft.com/zh-cn/windows-hardware/drivers/debugger/x64-architecture
https://learn.microsoft.com/zh-cn/cpp/build/stack-usage?source=recommendations&view=msvc-170
