日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

kernel 3.10内核源码分析--内核栈及堆栈切换

發布時間:2025/3/15 编程问答 85 豆豆
生活随笔 收集整理的這篇文章主要介紹了 kernel 3.10内核源码分析--内核栈及堆栈切换 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
1、概念
Linux中有3種棧:
1)用戶棧。當進程處于用戶態時使用,位于進程地址空間(用戶態部分(如:0-0xc0000000))底部,用戶態分配局部變量和函數調用時時,使用該棧,跟平時我們見到和理解的一樣,就是虛擬地址空間中的一段。
2)內核棧。跟用戶棧獨立,屬于進程,即每個進程都有自己的內核棧,單獨分配,大小為8k,跟thread_info結構放在一起,在用戶態和內核態切換時,需要進行切換。
3)中斷棧。老版本內核中默認認跟內核棧共享,新版本內核中與內核棧獨立,且軟中斷和硬中斷單獨使用自己的中斷棧。中斷、異常、軟中斷使用此棧。 ? ?
本文主要講解內核棧、用戶棧和內核棧切換的相關實現。

2、實現
1)內核棧定義和實現
內核棧跟thread_info結構放在一起,共用一個union:thread_union,
union thread_union {
? ? struct thread_info thread_info;
? ? unsigned long stack[THREAD_SIZE/sizeof(long)];
};
2)其它與內核棧相關的定義
a、task_struct.thread.sp0
任務描述符(task_struct)中的thread成員(thread_struct)用于保存進程上下文信息,包含主要寄存器信息,在進程上下文切換(如調度)時使用,thread成員定義為thread_struct結構體,其中的sp0成員用于保存內核棧指針。
b、task_struct.stack
任務描述符(task_struct)中的stack成員是新版本內核中新加入的成員,同樣指向內核棧頂,用于替代老版本中的thread_info成員,由于thread_info和內核棧實際是放在一起的,共享同一個聯合體thread_union,可以相互轉換,所以該修改并無本質區別。

點擊(此處)折疊或打開

  • /*進程描述符,每個進程(線程)都由此結構描述*/
  • struct task_struct {
  • ...
  • /*Fixme:新內核版本中去除了thread_info成員,用這個代替(thread_info和內核棧放在一起)*/
  • void *stack;
  • ...
  • /*進程上下文,包含主要寄存器信息,在進程上下文切換時使用*/
  • struct thread_struct thread;
  • ...
  • }

  • 點擊(此處)折疊或打開

  • struct thread_struct {
  • ...
  • /*內核堆棧(與thread_info放在一起,共享thread_union聯合體,大小為8k)的指針。TSS中有相應的字段,在特權級發生變化時,硬件會自動從TSS中讀取sp0,并進行堆棧切換。*/
  • unsigned long????sp0;
  • ...
  • }

  • c、kernel_stack per-CPU變量
    用于指向當前CPU上運行的進程的內核棧,由于內核棧與thread_info是放在一起的,所以,內核中也用這個變量來獲取當前進程的thread_info:

    點擊(此處)折疊或打開

  • /*
  • ??* 取當前進程的thread_info,該信息與內核棧(或中斷棧)公用聯合體(thread_union或irq_ctx),
  • ??* 且作為per-CPU變量(kernel_stack)放在了指定區域。
  • ??*/
  • static inline struct thread_info *current_thread_info(void)
  • {
  • struct thread_info *ti;
  • ti = (void *)(this_cpu_read_stable(kernel_stack) +
  • ?????KERNEL_STACK_OFFSET - THREAD_SIZE);
  • return ti;
  • }

  • d、tss_struct.sp0

    TSS任務狀態段是X86架構中包含的一個特殊的段,用戶保存硬件上下文,包含了當前進程的特權級(ring)信息和寄存器信息。在進程切換時使用,Linux中使用的情況比較少,而用戶棧和內核棧的切換就是其中一處關鍵的應用。Linux內核定義了tss_struct結構體來描述該段中的內容,其中的x86_tss(x86_hw_tss結構)中保存了相應的硬件狀態信息,其中sp0即為內核態(ring0)中的堆棧指針,ss0為內核態堆棧段寄存器,sp為用戶態(ring3)堆棧指針,Linux中主要使用了這幾個字段。

    點擊(此處)折疊或打開

  • /*用于描述TSS段中的內容*/
  • struct tss_struct {
  • /*
  • * The hardware state:
  • */
  • /*硬件狀態信息*/
  • struct x86_hw_tss????x86_tss;


  • /*
  • * The extra 1 is there because the CPU will access an
  • * additional byte beyond the end of the IO permission
  • * bitmap. The extra byte must be all 1 bits, and must
  • * be within the limit.
  • */
  • /*IO權位圖*/
  • unsigned long????io_bitmap[IO_BITMAP_LONGS + 1];


  • /*
  • * .. and then another 0x100 bytes for the emergency kernel stack:
  • */
  • /*備用內核棧*/
  • unsigned long????stack[64];


  • } ____cacheline_aligned;

  • 點擊(此處)折疊或打開

  • /* This is the TSS defined by the hardware. */
  • struct x86_hw_tss {
  • unsigned short????back_link, __blh;
  • unsigned long????sp0; /*內核棧指針*/
  • unsigned short????ss0, __ss0h; /*內核棧段描述符*/
  • unsigned long????sp1;
  • /* ss1 caches MSR_IA32_SYSENTER_CS: */
  • unsigned short????ss1, __ss1h;
  • unsigned long????sp2;
  • unsigned short????ss2, __ss2h;
  • unsigned long????__cr3;
  • unsigned long????ip;
  • unsigned long????flags;
  • unsigned long????ax;
  • unsigned long????cx;
  • unsigned long????dx;
  • unsigned long????bx;
  • unsigned long????sp; /*用戶態棧指針*/
  • unsigned long????bp;
  • unsigned long????si;
  • unsigned long????di;
  • unsigned short????es, __esh;
  • unsigned short????cs, __csh;
  • unsigned short????ss, __ssh;
  • unsigned short????ds, __dsh;
  • unsigned short????fs, __fsh;
  • unsigned short????gs, __gsh;
  • unsigned short????ldt, __ldth;
  • unsigned short????trace;
  • unsigned short????io_bitmap_base;


  • } __attribute__((packed))

  • 內核中定義針對每個CPU定義了一個了tss_struct結構體類型變量init_tss,在進程上下文切換(堆棧切換)時使用

    點擊(此處)折疊或打開

  • /*
  • ?* per-CPU TSS segments. Threads are completely 'soft' on Linux,
  • ?* no more per-task TSS's. The TSS size is kept cacheline-aligned
  • ?* so they are allowed to end up in the .data..cacheline_aligned
  • ?* section. Since TSS's are completely CPU-local, we want them
  • ?* on exact cacheline boundaries, to eliminate cacheline ping-pong.
  • ?*/
  • DEFINE_PER_CPU_SHARED_ALIGNED(struct tss_struct, init_tss) = INIT_TSS;

  • INIT_TSS定義如下:

    點擊(此處)折疊或打開

  • /*
  • ?* Note that the .io_bitmap member must be extra-big. This is because
  • ?* the CPU will access an additional byte beyond the end of the IO
  • ?* permission bitmap. The extra byte must be all 1 bits, and must
  • ?* be within the limit.
  • ?*/
  • #define INIT_TSS {???? \
  • .x86_tss = {???? \
  • .sp0????= sizeof(init_stack) + (long)&init_stack, \
  • .ss0????= __KERNEL_DS,???? \
  • .ss1????= __KERNEL_CS,???? \
  • .io_bitmap_base????= INVALID_IO_BITMAP_OFFSET,???? \
  • },???? \
  • .io_bitmap????= { [0 ... IO_BITMAP_LONGS] = ~0 },???? \
  • }
  • init_stack定義為:

    點擊(此處)折疊或打開

  • #define init_stack????(init_thread_union.stack)
  • 即將內核棧(sp0)指向了init_thread_union的內核棧頂。 init_thread_union為初始的任務描述符:

    點擊(此處)折疊或打開

  • union thread_union init_thread_union __init_task_data =
  • { INIT_THREAD_INFO(init_task) };
  • struct task_struct init_task = INIT_TASK(init_task);
  • #define INIT_TASK(tsk)????\
  • {????\
  • .state????= 0,????\
  • .stack????= &init_thread_info,????\
  • .usage????= ATOMIC_INIT(2),????\
  • .flags????= PF_KTHREAD,????\
  • .prio????= MAX_PRIO-20,????\
  • .static_prio????= MAX_PRIO-20,????\
  • .normal_prio????= MAX_PRIO-20,????\
  • .policy????= SCHED_NORMAL,????\
  • .cpus_allowed????= CPU_MASK_ALL,????\
  • .nr_cpus_allowed= NR_CPUS,????\
  • .mm????= NULL,
  • ...
  • }

  • 3)內核棧分配
    內核在何時分配?
    答案是在fork時。
    Linux中進程均由fork創建(init除外),在fork時即會創建相應的內核棧,相應流程為:
    do_fork->copy_process->dup_task_struct->alloc_thread_info_node

    點擊(此處)折疊或打開

  • /*分配thread_info,即分配內核棧*/
  • static struct thread_info *alloc_thread_info_node(struct task_struct *tsk,
  • ?int node)
  • {
  • /*THREAD_SIZE_ORDER為1,即2頁,即內核棧和thread_info共用的空間大小為8k*/
  • struct page *page = alloc_pages_node(node, THREADINFO_GFP_ACCOUNTED,
  • ????THREAD_SIZE_ORDER);


  • return page ? page_address(page) : NULL;
  • }

  • 點擊(此處)折疊或打開

  • static struct task_struct *dup_task_struct(struct task_struct *orig)
  • {
  • ...
  • /*分配thread_info,即分配內核棧*/
  • ti = alloc_thread_info_node(tsk, node);
  • ...
  • /*設置新進程的內核棧(stack成員指向)為新分配的thread_info,如此即設置好了內核棧*/
  • tsk->stack = ti;
  • ...
  • }
  • 4)用戶棧和內核棧切換
    我們知道,x86硬件結構中,SS為堆棧段寄存器,ESP為堆棧指針寄存器,而這兩個寄存器在每個CPU上都是唯一的,但是Linux中有多種棧(用戶棧、內核棧、中斷棧),如果都需要使用的話,那就必須在各個棧之間進行切換。
    當CPU運行的特權級(ring)發生變化時,就需要切換相應的堆棧,Linux中,只使用了兩個ring:ring0(內核態)和ring3(用戶態),當發生內核態和用戶態間的狀態切換時,需要考慮堆棧的切換,通常的切換時機有:系統調用、中斷和異常及其返回處。
    前面我們已經了解了內核棧的概念和定義,那么如何進行堆棧切換呢?那就需要使用上面描述的TSS段了,如之前所述,tss_struct中包括了內核棧指針sp0和內核堆棧段寄存器ss0。

    (1)用戶棧到內核棧的切換
    X86硬件結構中,中斷、異常和系統調用都是通過中斷門或陷阱門實現的,在通過中斷門或陷阱門時,硬件會自動利用TSS,完成堆棧切換的工作。硬件完成的操作包括:
    ? a、找到ISR的入口。具體包括:確定中斷或異常相關的中斷向量、讀取IDTR寄存器獲取IDT表地址、從IDT表中讀取中斷向量對應的項、讀取GDTR寄存器獲取GDT表地址,在GDT表中查找IDT表項中的段選擇符標識的段描述符,該描述符中指定了中斷/異常處理程序(ISR)所在的段基址,結合IDT表項中的段偏移地址,即可找到ISR的入口地址。
    ? b、權限檢查。確認中斷的來源是否合法,主要比對當前特權級(CS寄存器的低兩位)和段描述符(GDT中)的DPL比較。如果CPL小于DPL,就產生GP(通用保護)異常。對于異常,還需做進一步的安全檢查:比對CPL與IDT中的門描述符的DPL,如果DPL小于CPL,也產生GP(通用保護)異常,由此可以避免用戶應用程序訪問特殊的陷阱門和中斷門。?
    ? c、檢查特權級的變化。如果ring發生了變化(通常是從ring3到ring0,即用戶態切換到內核態),即CPL與段描述符的DPL不同,則進行一下處理(這里實際上進行了堆棧切換):?
    ????c1. 讀tr寄存器,獲取TSS段。?
    ????c2. 讀取TSS中的新特權級(內核態)的堆棧段和堆棧指針,將其load到SS和ESP寄存器。?
    ????c3. 在新特權級的棧(內核棧)中保存原始的(即用戶態的)SS和ESP的值。
    ? d. 如果發生的是異常,則將引起異常的指令地址裝載cs和eip寄存器,如此可以使這條指令在異常處理程序執行完后能被再次執行,這也是中斷和異常的主要區別之一;如果發生的中斷,則跳過此步驟。
    ? e. 在新棧(內核棧)中壓入eflag、cs和eip。?
    ? f. 如果是異常且產生了硬件出錯碼,則將它壓入棧中。?
    ? g. 用之前通過IDT和GDT獲取到的ISR的入口地址和段選擇符裝載EIP和CS寄存器,如此即可開始執行ISR。?
    再次強調:上述操作均由硬件自動完成。從上述的步驟c可以看出,當用戶態切換到內核態時,用戶棧切換到內核棧實質是由硬件自動完成的,軟件需要做的是預先設置好TSS中的相關內容(比如sp0和ss0)。

    (2)內核棧到用戶棧的切換
    該過程實際是“用戶棧到內核棧切換”的逆過程,發生時機在系統調用、中斷和異常的返回處,實際的切換過程還是由硬件自動完成的。
    中斷或異常返回時,必然會執行iret指令,然后將控制器交回給之前被中斷打斷的進程,硬件自動完成如下操作:
    ??a. 從當前棧(內核棧)中彈出cs、eip和eflag,并load到相應的寄存器中寄存器。(如之前有硬件錯誤碼入棧,需要先彈出這個錯誤碼)。?
    ??b. 權限檢查。比對ISR的CPL是否等于cs中的低兩位的值。如果是,iret終止返回;否則,轉入下一步。?
    ??c. 從當前棧(內核棧)中彈出之前壓入的用戶態堆棧相關的ss和esp,并load到相應寄存器,至此,即完成了從內核棧到用戶棧的切換。?
    ? d. 后續處理。主要包括:檢查ds、es、fs及gs段寄存器,如果其中一個寄存器包含的選擇符是一個段描述符,并且其DPL值小于CPL,那么,清相關的段寄存器。目的是為了防止用戶態的程序利用內核以前所用的段寄存器,以防止惡意用戶程序利用其訪問內核地址空間。?

    (3)進程上下文切換時的堆棧切換
    內核調度時,當被選中的next進程不是current進程時,會發生上下文切換,此時也必然會涉及堆棧切換。這里涉及到兩個相關的問題:
    a、從current進程的堆棧切換到next進程的堆棧
    由于調度肯定發生在內核態,那么進程上下文切換時,也必然處于內核態,那么此時的堆棧切換實質為current進程的內核棧到next進程內核棧的切換。相應工作在switch_to宏中用匯編實現,通過next進程的棧指針(內核態)裝載到ESP寄存器中實現。
    ? ? "movl %[next_sp],%%esp\n\t" /* restore ESP ? */ \
    具體代碼如下:

    點擊(此處)折疊或打開

  • /*
  • ??* 上下文切換,在schedule中調用,current進程調度出去,當該進程被再次調度到時,重新從__switch_to后面開始執行
  • ??* prev:被替換的進程
  • ??* next:被調度的新進程
  • ??* last:當切換回原來的進程(prev)后,被替換的另外一個進程。
  • ??*/
  • #define switch_to(prev, next, last)????\
  • do {????\
  • /*????\
  • * Context-switching clobbers all registers, so we clobber????\
  • * them explicitly, via unused output variables.????\
  • * (EAX and EBP is not listed because EBP is saved/restored????\
  • * explicitly for wchan access and EAX is the return value of????\
  • * __switch_to())????\
  • */????\
  • unsigned long ebx, ecx, edx, esi, edi;????\
  • \
  • asm volatile("pushfl\n\t"????/* save flags */????/*將eflags寄存器值壓棧*/\
  • ????"pushl %%ebp\n\t"????/* save EBP */????/*將EBP壓棧*/\
  • /*將當前棧指針(內核態)保存到prev進程的thread.sp中*/
  • ????"movl %%esp,%[prev_sp]\n\t"????/* save ESP */ \
  • ????/*將next進程的棧指針(內核態)裝載到ESP寄存器中*/
  • ????"movl %[next_sp],%%esp\n\t"????/* restore ESP */ \
  • ????/*保存"標號1"的地址到prev進程的thread.ip,以便當prev進程重新被調度運行時,可以從"標號1處"重新開始執行*/
  • ????"movl $1f,%[prev_ip]\n\t"????/* save EIP */????\
  • ????/*
  • ?????????* 將next進程的IP(通常都是"標號1"的地址,因為通常都是經歷過這里的調度過程的,上一行代碼中即保存了這個IP)
  • ???* 壓入當前的(即next進程的)堆棧中。結合后面的jmp指令(注意:不是call指令)一起理解,當__switch_to執行完ret返回時,
  • ???* 會自動從當前的堆棧中彈出該地址作為函數的返回地址接著執行,如此即可實現新進程的運行。
  • ???????????????????????*/
  • ????"pushl %[next_ip]\n\t"????/* restore EIP */????\
  • ????__switch_canary????\
  • ????/*
  • ?????????*jmp到__switch_to函數執行,當此函數返回時,自動跳轉到[next_ip]開始執行,實現新進程的調度。注意不是call,jmp指令
  • ?????????* 不會自動將當前地址壓棧,call會自動壓棧
  • ?????????*/
  • ????"jmp __switch_to\n"????/* regparm call */????\
  • ????/*當prev進程再次被調度到時,從這里開始執行*/
  • ????"1:\t"????\
  • ????/*恢復EBP*/
  • ????"popl %%ebp\n\t"????/* restore EBP */????\
  • ????/*恢復eflags*/
  • ????"popfl\n"????/* restore flags */????\
  • \
  • ????/* output parameters */????\
  • ????: [prev_sp] "=m" (prev->thread.sp),????\
  • ??????[prev_ip] "=m" (prev->thread.ip),????\
  • ??????"=a" (last),????\
  • \
  • ??????/* clobbered output registers: */????\
  • ??????"=b" (ebx), "=c" (ecx), "=d" (edx),????\
  • ??????"=S" (esi), "=D" (edi)????\
  • ??????\
  • ??????__switch_canary_oparam????\
  • \
  • ??????/* input parameters: */????\
  • ????: [next_sp] "m" (next->thread.sp),????\
  • ??????[next_ip] "m" (next->thread.ip),????\
  • ??????\
  • ??????/* regparm parameters for __switch_to(): */????\
  • ??????[prev] "a" (prev),????\
  • ??????[next] "d" (next)????\
  • \
  • ??????__switch_canary_iparam????\
  • \
  • ????: /* reloaded segment registers */????\
  • "memory");????\
  • } while (0)

  • b、TSS中內核棧(sp0)的切換
    由于Linux的具體實現中,TSS不是針對每進程,而是針對每CPU的,即每個CPU對應一個tss_struct,那在進程上下文切換時,需要考慮當前CPU上TSS中的內容的更新,其實就是內核棧指針的更新,更新后,當新進程再次進入到內核態執行時,才能確保CPU硬件能從TSS中自動讀取到正確的內核棧指針(sp0)的值,以保證從用戶態切換到內核態時,相應的堆棧切換正常。
    相應的切換在__switch_to中由load_sp0函數完成,

    點擊(此處)折疊或打開

  • __notrace_funcgraph struct task_struct *
  • __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
  • {
  • ...
  • struct tss_struct *tss = &per_cpu(init_tss, cpu);
  • ...
  • /*
  • * Reload esp0.
  • */
  • /*將next進程的內核棧指針(next->thread->sp0)值更新到當前CPU的TSS中*/
  • load_sp0(tss, next);
  • ...
  • }

  • 點擊(此處)折疊或打開

  • static inline void load_sp0(struct tss_struct *tss,
  • ???struct thread_struct *thread)
  • {
  • native_load_sp0(tss, thread);
  • }

  • 點擊(此處)折疊或打開

  • static inline void
  • native_load_sp0(struct tss_struct *tss, struct thread_struct *thread)
  • {
  • /*將thread中的內核棧指針賦給TSS中的相應字段*/
  • tss->x86_tss.sp0 = thread->sp0;
  • #ifdef CONFIG_X86_32
  • /* Only happens when SEP is enabled, no need to test "SEP"arately: */
  • if (unlikely(tss->x86_tss.ss1 != thread->sysenter_cs)) {
  • tss->x86_tss.ss1 = thread->sysenter_cs;
  • wrmsr(MSR_IA32_SYSENTER_CS, thread->sysenter_cs, 0);
  • }
  • #endif
  • }

  • (4)TSS初始化
    如前面描述,Linux內核中使用tss_struct來描述TSS,那么硬件上的TSS如何跟內核中的tss_struct關聯起來的呢?
    答案是在內核初始化的過程中,會進行相應的初始化,本質上是將TSS中相應的base地址設置為tss_struct per-CPU變量init_tss的地址,如此以來,修改init_tss后,相應的值即會體現到TSS(硬件)中。 TSS段初始化流程如下:
    a、BSP上的初始化流程
    rest_init->kernel_init->kernel_init_freeable->smp_init->cpu_up->native_cpu_up->do_boot_cpu->start_secondary->cpu_init->set_tss_desc->__set_tss_desc
    b、ASP上的初始化流程
    start_kernel->trap_init->cpu_init->set_tss_desc->__set_tss_desc

    點擊(此處)折疊或打開

  • void __cpuinit cpu_init(void)
  • {
  • ...
  • t = &per_cpu(init_tss, cpu);
  • ...
  • load_sp0(t, thread);
  • set_tss_desc(cpu, t);
  • load_TR_desc();
  • ...
  • }

  • 點擊(此處)折疊或打開

  • /*
  • ??* 設置TSS段描述符,將GDT中TSS段描述符中的base地址設置為指定的addr,即讓TSS段指向指定的addr.
  • ??*/
  • static inline void __set_tss_desc(unsigned cpu, unsigned int entry, void *addr)
  • {
  • /*獲取per-CPU的gdt(全局描述符表)*/
  • struct desc_struct *d = get_cpu_gdt_table(cpu);
  • tss_desc tss;


  • /*
  • * sizeof(unsigned long) coming from an extra "long" at the end
  • * of the iobitmap. See tss_struct definition in processor.h
  • *
  • * -1? seg base+limit should be pointing to the address of the
  • * last valid byte
  • */
  • /*TSS段描述符中的base地址設置為指定的addr,即讓TSS段指向指定的addr.*/
  • set_tssldt_descriptor(&tss, (unsigned long)addr, DESC_TSS,
  • ?????IO_BITMAP_OFFSET + IO_BITMAP_BYTES +
  • ?????sizeof(unsigned long) - 1);
  • /*設置GDT中的TSS對應的entry為新設置的tss*/
  • write_gdt_entry(d, entry, &tss, DESC_TSS);
  • }

  • 點擊(此處)折疊或打開

  • static inline void set_tssldt_descriptor(void *d, unsigned long addr, unsigned type, unsigned size)
  • {
  • #ifdef CONFIG_X86_64
  • struct ldttss_desc64 *desc = d;


  • memset(desc, 0, sizeof(*desc));


  • desc->limit0????= size & 0xFFFF;
  • /*設置描述符的base地址為addr*/
  • desc->base0????= PTR_LOW(addr);
  • desc->base1????= PTR_MIDDLE(addr) & 0xFF;
  • desc->type????= type;
  • desc->p????= 1;
  • desc->limit1????= (size >> 16) & 0xF;
  • desc->base2????= (PTR_MIDDLE(addr) >> 8) & 0xFF;
  • desc->base3????= PTR_HIGH(addr);
  • #else
  • pack_descriptor((struct desc_struct *)d, addr, size, 0x80 | type, 0);
  • #endif
  • }

  • 原文地址: http://blog.chinaunix.net/uid-14528823-id-4739291.html

    總結

    以上是生活随笔為你收集整理的kernel 3.10内核源码分析--内核栈及堆栈切换的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。