Linux中斷一網(wǎng)打盡(2) - IDT及中斷處理的實(shí)現(xiàn)

Linux中斷一網(wǎng)打盡(1) - IDT及中斷處理的實(shí)現(xiàn)

通過閱讀本文您可以了解到:

  • IDT是什么 矾湃;
  • IDT如何被初始化谈宛;
  • 什么是門昂芜;
  • 傳統(tǒng)系統(tǒng)調(diào)用是如何實(shí)現(xiàn)的哄辣;
  • 硬件中斷的實(shí)現(xiàn);
如何設(shè)置IDT
IDT 中斷描述符表定義

中斷描述符表簡(jiǎn)單來說說是定義了發(fā)生中斷/異常時(shí),CPU按這張表中定義的行為來處理對(duì)應(yīng)的中斷/異常正歼。

#define IDT_ENTRIES         256
gate_desc idt_table[IDT_ENTRIES] __page_aligned_bss;

從上面我們可以知道辐马,其包含了256項(xiàng),它是一個(gè)gate_desc的數(shù)據(jù),其下標(biāo)0-256就表示中斷向量喜爷,gate_desc我們?cè)谙旅骜R上介紹冗疮。

中斷描述符項(xiàng)定義
  • 當(dāng)中斷發(fā)生,cpu獲取到中斷向量后檩帐,查找IDT中斷描述符表得到相應(yīng)的中斷描述符术幔,再根據(jù)中斷描述符記錄的信息來作權(quán)限判斷,運(yùn)行級(jí)別轉(zhuǎn)換湃密,最終調(diào)用相應(yīng)的中斷處理程序诅挑;

  • 這里涉及到Linux kernel的分段式內(nèi)存管理,我們這里不詳細(xì)展開泛源,有興趣的同學(xué)可以自行學(xué)習(xí)拔妥。如下簡(jiǎn)述之:

    1. 我們知道CPU只認(rèn)識(shí)邏輯地址,邏輯地址經(jīng)分段處理轉(zhuǎn)換成線性地址达箍,線性地址經(jīng)分頁(yè)處理最終轉(zhuǎn)換成物理地址没龙,這樣就可以從內(nèi)存中讀取了;

    2. 邏輯地址你可以簡(jiǎn)單認(rèn)為就是CPU執(zhí)行代碼時(shí)從CS(代碼段寄存器) : IP (指令計(jì)數(shù)寄存器)中加載的代碼缎玫,實(shí)際上通過CS可以得到邏輯地址的基地址硬纤,再加上IP這個(gè)相對(duì)于基地址的偏移量,就得到真正的邏輯地址赃磨;

    3. CS寄存器16位筝家,它不會(huì)包含真正的基地址,它一般被稱為段選擇子煞躬,包括一個(gè)index索引肛鹏,指向GDTLDT的一項(xiàng);一個(gè)指示位恩沛,指示index索引是屬于GDT還是LDT; 還有CPL, 表明當(dāng)前代碼運(yùn)行權(quán)限;

    4. GDT: 全局描述符表缕减,每一項(xiàng)記錄著相應(yīng)的段基址雷客,段大小,段的訪問權(quán)限DPL等桥狡,到這里終于可以獲取到段基地址了搅裙,再加上之前IP寄存器里存放的偏移量,真正的邏輯地址就有了裹芝。

    5. 附上簡(jiǎn)圖:


      idt2.jpg
  • 我們先看中斷描述符的定義:

    struct gate_struct {
      u16     offset_low;
      u16     segment;
      struct idt_bits bits;
      u16     offset_middle;
    #ifdef CONFIG_X86_64
      u32     offset_high;
      u32     reserved;
    #endif
    } __attribute__((packed));
    

    其中:

    1. offset_high,offset_middleoffset_low合起來就是中斷處理函數(shù)地址的偏移量部逮;

    2. segment就是相應(yīng)的段選擇子,根據(jù)它在GDT中查找可以最終獲取到段基地址嫂易;

    3. bits是該中斷描述符的一些屬性值:

      struct idt_bits {
         u16     ist : 3,
                 zero    : 5,
                 type    : 5,
                 dpl : 2,
                 p   : 1;
      } __attribute__((packed));
      

      ist表示此中斷處理函數(shù)是使用pre-cpu的中斷棧兄朋,還是使用IST的中斷棧;

      type表示所中斷是何種類型,目前有以下四種:

      enum {
         GATE_INTERRUPT = 0xE, //中斷門
         GATE_TRAP = 0xF, // 陷入門
         GATE_CALL = 0xC, // 調(diào)用門
         GATE_TASK = 0x5, // 任務(wù)門
      };
      

      的概念這里主要用作權(quán)限控制怜械,我們從一個(gè)區(qū)域進(jìn)到另一個(gè)區(qū)域需要通過一扇門颅和,有門禁權(quán)限才可以通過傅事,因此 dpl就是這個(gè)權(quán)限,實(shí)際中我們一般稱為RPL峡扩;

      我們后面會(huì)通過一個(gè)例子來講一下CPL,RPLDPL三者之間的關(guān)系蹭越。

IDT 中斷描述符表本身的存儲(chǔ)

IDT 中斷描述符表的物理地址存儲(chǔ)在IDTR寄存器中,這個(gè)寄存器存儲(chǔ)了IDT的基地址和長(zhǎng)度教届。查詢時(shí)响鹃,從 IDTR 拿到 base address ,加上向量號(hào) * IDT entry size案训,即可以定位到對(duì)應(yīng)的表項(xiàng)(gate)茴迁。

idt1.jpg
設(shè)置IDT
  • 設(shè)置中斷門類型的IDT描述符

    static void set_intr_gate(unsigned int n, const void *addr)
    {
      struct idt_data data;
    
      BUG_ON(n > 0xFF);
    
      memset(&data, 0, sizeof(data));
      data.vector = n; // 中斷向量
      data.addr   = addr; // 中斷處理函數(shù)的地址
      data.segment    = __KERNEL_CS; // 段選擇子
      data.bits.type  = GATE_INTERRUPT; // 類型
      data.bits.p = 1;
    
      idt_setup_from_table(idt_table, &data, 1, false);
    }
    

    上面的函數(shù)主要是填充好idt_data,然后調(diào)用idt_setup_from_table;

  • idt_setup_from_table:

    static void
    idt_setup_from_table(gate_desc *idt, const struct idt_data *t, int size, bool sys)
    {
      gate_desc desc;
    
      for (; size > 0; t++, size--) {
          idt_init_desc(&desc, t);
          write_idt_entry(idt, t->vector, &desc);
          if (sys)
              set_bit(t->vector, system_vectors);
      }
    }
    

    首先使用 idt_data結(jié)構(gòu)來填充中斷描述符變量idt_init_desc, 然后將這個(gè)中斷描述符變量copy進(jìn)idt_table萤衰。

    看堕义,就是這么簡(jiǎn)單~~~

  • gate_desc的多種初始化方法

    因?yàn)?code>gate_desc是通過ida_dat填充的,所以這里關(guān)鍵是idt_data的初始化脆栋,我們?cè)敿?xì)看一下:

    /* Interrupt gate 
    中斷門倦卖,DPL = 0
    只能從內(nèi)核調(diào)用
    */
    #define INTG(_vector, _addr)              \
      G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL0, __KERNEL_CS)
    
    /* System interrupt gate
    系統(tǒng)中斷門,DPL = 3
    可以從用戶態(tài)調(diào)用椿争,比如系統(tǒng)調(diào)用
    */
    #define SYSG(_vector, _addr)              \
      G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL3, __KERNEL_CS)
    
    /*
     * Interrupt gate with interrupt stack. The _ist index is the index in
     * the tss.ist[] array, but for the descriptor it needs to start at 1.
     中斷門, DPL = 0
     只能從內(nèi)核態(tài)調(diào)用怕膛,使用TSS.IST[]作為中斷棧 
     */
    #define ISTG(_vector, _addr, _ist)            \
      G(_vector, _addr, _ist + 1, GATE_INTERRUPT, DPL0, __KERNEL_CS)
    
    /* Task gate
    任務(wù)門, DPL = 0
    只能作內(nèi)核態(tài)調(diào)用 
    */
    #define TSKG(_vector, _gdt)               \
      G(_vector, NULL, DEFAULT_STACK, GATE_TASK, DPL0, _gdt << 3)
    
    

    我們?cè)賮砜聪?code>G這個(gè)宏的實(shí)現(xiàn):

    #define G(_vector, _addr, _ist, _type, _dpl, _segment)    \
      {                       \
          .vector     = _vector,      \
          .bits.ist   = _ist,         \
          .bits.type  = _type,        \
          .bits.dpl   = _dpl,         \
          .bits.p     = 1,            \
          .addr       = _addr,        \
          .segment    = _segment,     \
      }
    

    實(shí)際上就是填充idt_data的各個(gè)字段秦踪。

傳統(tǒng)系統(tǒng)調(diào)用的實(shí)現(xiàn)

這里所說的傳統(tǒng)系統(tǒng)調(diào)用主要指舊的32位系統(tǒng)使用 int 0x80軟件中斷來進(jìn)入內(nèi)核態(tài)褐捻,實(shí)現(xiàn)的系統(tǒng)調(diào)用。因?yàn)檫@種傳統(tǒng)系統(tǒng)調(diào)用方式需要進(jìn)入內(nèi)核后作權(quán)限驗(yàn)證椅邓,還要切換內(nèi)核棧后作大量壓棧方式柠逞,調(diào)用結(jié)束后清理?xiàng)W骰謴?fù),兩個(gè)字太慢景馁,后來CPU從硬件上支持快速系統(tǒng)調(diào)用sysenter/sysexit, 再后來又發(fā)展到syscall/sysret板壮, 這兩種都不需要通過中斷方式進(jìn)入內(nèi)核態(tài),而是直接轉(zhuǎn)換到內(nèi)核態(tài)合住,速度快了很多绰精。

傳統(tǒng)系統(tǒng)調(diào)用相關(guān) IDT 的設(shè)置
  • Linux系統(tǒng)啟動(dòng)過程中內(nèi)核壓解后最終都調(diào)用到start_kernel, 在這里會(huì)調(diào)用trap_init, 然后又會(huì)調(diào)用idt_setup_traps:

    void __init idt_setup_traps(void)
    {
      idt_setup_from_table(idt_table, def_idts, ARRAY_SIZE(def_idts), true);
    }
    

    我們來看這里的def_idts的定義:

    static const __initconst struct idt_data def_idts[] = {
      ....
    #if defined(CONFIG_IA32_EMULATION)
      SYSG(IA32_SYSCALL_VECTOR,   entry_INT80_compat),
    #elif defined(CONFIG_X86_32)
      SYSG(IA32_SYSCALL_VECTOR,   entry_INT80_32),
    #endif
    };
    

? 上面的SYSG(IA32_SYSCALL_VECTOR, entry_INT80_32)就是設(shè)置系統(tǒng)調(diào)用的異常中斷處理程序,其中 #define IA32_SYSCALL_VECTOR 0x80

再看一下SYSG的定義:

#define SYSG(_vector, _addr)                \
    G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL3, __KERNEL_CS)

它初始化一個(gè)中斷門透葛,權(quán)限是DPL3, 因此從用戶態(tài)是允許發(fā)起系統(tǒng)調(diào)用的笨使。

  • 我們調(diào)用系統(tǒng)調(diào)用,不大可能自已手寫匯編代碼僚害,都是通過glibc來調(diào)用硫椰,基本流程是保存參數(shù)到寄存器,然后保存系統(tǒng)調(diào)用向量號(hào)到eax寄存器,然后調(diào)用int 0x80進(jìn)入內(nèi)核態(tài)最爬,切換到內(nèi)核棧涉馁,將用戶態(tài)時(shí)的ss/sp/eflags/cs/ip/error code依次壓入內(nèi)核棧。

  • entry_INT80_32系統(tǒng)調(diào)用對(duì)應(yīng)的中斷處理程序

    ENTRY(entry_INT80_32)
      ASM_CLAC
      pushl   %eax            /* pt_regs->orig_ax */
    
      SAVE_ALL pt_regs_ax=$-ENOSYS switch_stacks=1    /* save rest */
    
      TRACE_IRQS_OFF
    
      movl    %esp, %eax
      call    do_int80_syscall_32
    .Lsyscall_32_done:
    ...
    .Lirq_return:
    
      INTERRUPT_RETURN
    
    ...
    ENDPROC(entry_INT80_32)
    

    我們略去了中間的一些細(xì)節(jié)部分爱致,可以看到首先將中斷向量號(hào)壓棧烤送,再保存所有當(dāng)前的寄存器值到pt_regs, 保存當(dāng)前棧指針到%eax寄存器,最后再調(diào)用 do_int80_syscall_32, 這個(gè)函數(shù)中就會(huì)執(zhí)行具體的中斷處理糠悯,然后INTERRUPT_RETURN恢復(fù)棧帮坚,作好返回用戶態(tài)的準(zhǔn)備。

  • do_int80_syscall_32調(diào)用 do_syscall_32_irqs_on,我們看一下其實(shí)現(xiàn):

static __always_inline void do_syscall_32_irqs_on(struct pt_regs *regs)
  {
    struct thread_info *ti = current_thread_info();
    unsigned int nr = (unsigned int)regs->orig_ax;
  
  #ifdef CONFIG_IA32_EMULATION
    ti->status |= TS_COMPAT;
  #endif
  
    if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY) {
        nr = syscall_trace_enter(regs);
    }
  
    if (likely(nr < IA32_NR_syscalls)) {
        nr = array_index_nospec(nr, IA32_NR_syscalls);
  #ifdef CONFIG_IA32_EMULATION
        regs->ax = ia32_sys_call_table[nr](regs);
  #else
        regs->ax = ia32_sys_call_table[nr](
            (unsigned int)regs->bx, (unsigned int)regs->cx,
            (unsigned int)regs->dx, (unsigned int)regs->si,
            (unsigned int)regs->di, (unsigned int)regs->bp);
  #endif /* CONFIG_IA32_EMULATION */
    }
  
    syscall_return_slowpath(regs);
  }

通過中斷向量號(hào)nria32_sys_call_table中斷向量表中索引到具體的中斷處理函數(shù)然后調(diào)用之互艾,其結(jié)果最終合存入%eax寄存器试和。

一圖以蔽之
idt3.jpg
硬件中斷的實(shí)現(xiàn)
硬件中斷的IDT初始化和調(diào)用流程

這里我們不講解具體的代碼細(xì)節(jié),只關(guān)注流程 纫普。

硬件中斷相關(guān)IDT的初始化也是在Linux啟動(dòng)時(shí)完成阅悍,在start_kernel中通過調(diào)用init_IRQ完成,我們來看一下:

void __init init_IRQ(void)
{
    int i;
    for (i = 0; i < nr_legacy_irqs(); i++)
        per_cpu(vector_irq, 0)[ISA_IRQ_VECTOR(i)] = irq_to_desc(i);

    BUG_ON(irq_init_percpu_irqstack(smp_processor_id()));

    x86_init.irqs.intr_init(); // 即調(diào)用  native_init_IRQ
}

void __init native_init_IRQ(void)
{
    /* Execute any quirks before the call gates are initialised: */
    x86_init.irqs.pre_vector_init();

    idt_setup_apic_and_irq_gates();
    lapic_assign_system_vectors();

    if (!acpi_ioapic && !of_ioapic && nr_legacy_irqs())
        setup_irq(2, &irq2);
}

重點(diǎn)在于idt_setup_apic_and_irq_gates:

 */
void __init idt_setup_apic_and_irq_gates(void)
{
    int i = FIRST_EXTERNAL_VECTOR;
    void *entry;

    idt_setup_from_table(idt_table, apic_idts, ARRAY_SIZE(apic_idts), true);

    for_each_clear_bit_from(i, system_vectors, FIRST_SYSTEM_VECTOR) {
        entry = irq_entries_start + 8 * (i - FIRST_EXTERNAL_VECTOR);
        set_intr_gate(i, entry);
    }
}

其中的set_intr_gate用來初始化硬件相關(guān)的調(diào)用門昨稼,其對(duì)應(yīng)的中斷門處理函數(shù)在irq_entries_start中定義节视,它位于arch/x86/entry/entry_64.S中:

    .align 8
ENTRY(irq_entries_start)
    vector=FIRST_EXTERNAL_VECTOR
    .rept (FIRST_SYSTEM_VECTOR - FIRST_EXTERNAL_VECTOR)
    UNWIND_HINT_IRET_REGS
    pushq   $(~vector+0x80)         /* Note: always in signed byte range */
    jmp common_interrupt
    .align  8
    vector=vector+1
    .endr
END(irq_entries_start)

這段匯編實(shí)現(xiàn)對(duì)不大熟悉匯編的同學(xué)可能看起來有點(diǎn)暈,其實(shí)很簡(jiǎn)單它相當(dāng)于填充一個(gè)中斷處理函數(shù)的數(shù)組假栓,填充多少次呢? (FIRST_SYSTEM_VECTOR - FIRST_EXTERNAL_VECTOR)這就是次數(shù)寻行,數(shù)組的每一項(xiàng)都是一個(gè)函數(shù):

    UNWIND_HINT_IRET_REGS
    pushq   $(~vector+0x80)         /* Note: always in signed byte range */
    jmp common_interrupt

即先將中斷號(hào)壓棧,然后跳轉(zhuǎn)到common_interrupt執(zhí)行匾荆,可以看到這個(gè)common_interrupt是硬件中斷的通用處理函數(shù)拌蜘,它里面最主要的就是調(diào)用do_IRQ:

__visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
{
    struct pt_regs *old_regs = set_irq_regs(regs);
    struct irq_desc * desc;
    /* high bit used in ret_from_ code  */
    unsigned vector = ~regs->orig_ax;

    entering_irq();

    /* entering_irq() tells RCU that we're not quiescent.  Check it. */
    RCU_LOCKDEP_WARN(!rcu_is_watching(), "IRQ failed to wake up RCU");

    desc = __this_cpu_read(vector_irq[vector]);
    if (likely(!IS_ERR_OR_NULL(desc))) {
        if (IS_ENABLED(CONFIG_X86_32))
            handle_irq(desc, regs);
        else
            generic_handle_irq_desc(desc);
    } else {
        ack_APIC_irq();

        if (desc == VECTOR_UNUSED) {
            pr_emerg_ratelimited("%s: %d.%d No irq handler for vector\n",
                         __func__, smp_processor_id(),
                         vector);
        } else {
            __this_cpu_write(vector_irq[vector], VECTOR_UNUSED);
        }
    }

    exiting_irq();

    set_irq_regs(old_regs);
    return 1;
}

首先根據(jù)中斷向量號(hào)獲取到對(duì)應(yīng)的中斷描述符irq_desc, 然后調(diào)用generic_handle_irq來處理:

static inline void generic_handle_irq_desc(struct irq_desc *desc)
{
    desc->handle_irq(desc);
}

這里最終會(huì)調(diào)用到中斷描述符的handle_irq,因此另一個(gè)重點(diǎn)就是這個(gè)中斷描述符的設(shè)置了牙丽,它可以單開一篇文章來講简卧,我們暫不詳述了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末剩岳,一起剝皮案震驚了整個(gè)濱河市贞滨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌拍棕,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件勺良,死亡現(xiàn)場(chǎng)離奇詭異绰播,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)尚困,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門蠢箩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事谬泌√显希” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵掌实,是天一觀的道長(zhǎng)陪蜻。 經(jīng)常有香客問我,道長(zhǎng)贱鼻,這世上最難降的妖魔是什么宴卖? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮邻悬,結(jié)果婚禮上症昏,老公的妹妹穿的比我還像新娘。我一直安慰自己父丰,他們只是感情好肝谭,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蛾扇,像睡著了一般攘烛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上屁桑,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天医寿,我揣著相機(jī)與錄音,去河邊找鬼蘑斧。 笑死靖秩,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的竖瘾。 我是一名探鬼主播沟突,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼捕传!你這毒婦竟也來了惠拭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤庸论,失蹤者是張志新(化名)和其女友劉穎职辅,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體聂示,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡域携,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鱼喉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片秀鞭。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡趋观,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出锋边,到底是詐尸還是另有隱情皱坛,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布豆巨,位于F島的核電站剩辟,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏搀矫。R本人自食惡果不足惜抹沪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望瓤球。 院中可真熱鬧融欧,春花似錦、人聲如沸卦羡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)绿饵。三九已至欠肾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間拟赊,已是汗流浹背刺桃。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留吸祟,地道東北人瑟慈。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像屋匕,于是被迫代替她去往敵國(guó)和親葛碧。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容