MIT-6.828 Lab1實驗報告

来源:https://www.cnblogs.com/gatsby123/archive/2018/10/09/9759153.html
-Advertisement-
Play Games

Lab1:Booting a PC 概述 本文主要介紹lab1,從內容上分為三部分,part1簡單介紹了彙編語言,物理記憶體地址空間,BIOS。part2介紹了BIOS從磁碟0號扇區讀取boot loader到0000:7c00處,並將cs:ip設置成0000:7c00。boot loader主要做兩 ...


Lab1:Booting a PC

概述

本文主要介紹lab1,從內容上分為三部分,part1簡單介紹了彙編語言,物理記憶體地址空間,BIOS。part2介紹了BIOS從磁碟0號扇區讀取boot loader到0000:7c00處,並將cs:ip設置成0000:7c00。boot loader主要做兩件事:

  1. 創建兩個全局描述符表項(代碼段和數據段),然後進入保護模式
  2. 從磁碟載入kernel到記憶體

part3主要介紹進入內核後的一些操作,首先會開啟分頁模式。還介紹了格式化輸出,函數調用過程。
對應的lab主頁為:lab1

Part 1: PC Bootstrap

本課程使用的彙編使用AT&T語法,Brennan's Guide to Inline Assembly給出Intel語法和AT&T語法之間的一些對應關係。
物理地址記憶體空間可用下圖來描述:

+------------------+  <- 0xFFFFFFFF (4GB)
|      32-bit      |
|  memory mapped   |
|     devices      |
|                  |
/\/\/\/\/\/\/\/\/\/\

/\/\/\/\/\/\/\/\/\/\
|                  |
|      Unused      |
|                  |
+------------------+  <- depends on amount of RAM
|                  |
|                  |
| Extended Memory  |
|                  |
|                  |
+------------------+  <- 0x00100000 (1MB)
|     BIOS ROM     |
+------------------+  <- 0x000F0000 (960KB)
|  16-bit devices, |
|  expansion ROMs  |
+------------------+  <- 0x000C0000 (768KB)
|   VGA Display    |
+------------------+  <- 0x000A0000 (640KB)
|                  |
|    Low Memory    |
|                  |
+------------------+  <- 0x00000000

最早期的16-bit Intel 8088處理器僅支持1MB(0x00000000~0x000FFFFF)的物理定址能力。到了80286和80386處理器,分別支持16MB和4GB的物理定址能力。為了做到向後相容,保留了低1MB的記憶體佈局。
PC通電後會設置CS為0xf000,IP為0xfff0,也就是說第一條指令會在物理記憶體0xffff0處,該地址位於BIOS區域的尾部。
QEMU提供了調試功能,打開兩個終端,一個在lab目錄下執行make qemu-gdb,QEMU會在執行第一條指令前暫停,等待GDB的連接。另一個終端執行make gdb執行完後會出現如下輸出

GNU gdb (GDB) 6.8-debian
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i486-linux-gnu".
+ target remote localhost:26000
The target architecture is assumed to be i8086
[f000:fff0] 0xffff0:    ljmp   $0xf000,$0xe05b
0x0000fff0 in ?? ()
+ symbol-file obj/kern/kernel
(gdb) 

可以看到第一條指令確實在0xf000:0xfff0處,該條指令為ljmp $0xf000,$0xe05b跳轉到BIOS的前半部分。然後做一些初始化工作,最後從磁碟起始扇區載入512位元組到物理地址0x7c00處,並用jmp指令將CS:IP設置為0x0000:0x7c00,從而進入boot loader的控制。

Part 2: The Boot Loader

boot laoder代碼在boot/boot.S和boot/main.c中,主要做了兩件事:

  1. 從實模式進入保護模式
  2. 從磁碟載入kernel

先看boot/boot.S,

  cli                         # Disable interrupts
  cld                         # String operations increment
  # Set up the important data segment registers (DS, ES, SS).
  xorw    %ax,%ax             # Segment number zero
  movw    %ax,%ds             # -> Data Segment
  movw    %ax,%es             # -> Extra Segment
  movw    %ax,%ss             # -> Stack Segment

cli這條指令應該是被載入到0x7c00處的指令,也就是進入boot loader後執行的第一條指令。後面幾行主要就是設置段寄存器ds, es, ss為0。

  # Enable A20:
  #   For backwards compatibility with the earliest PCs, physical
  #   address line 20 is tied low, so that addresses higher than
  #   1MB wrap around to zero by default.  This code undoes this.
seta20.1:
  inb     $0x64,%al               # Wait for not busy
  testb   $0x2,%al
  jnz     seta20.1

  movb    $0xd1,%al               # 0xd1 -> port 0x64
  outb    %al,$0x64

seta20.2:
  inb     $0x64,%al               # Wait for not busy
  testb   $0x2,%al
  jnz     seta20.2

  movb    $0xdf,%al               # 0xdf -> port 0x60
  outb    %al,$0x60

這幾行主要是為了開啟A20,也就是處理器的第21根地址線。在早期8086處理器上每次到物理地址達到最高端的0xFFFFF時,再加1,就又會繞回到最低地址0x00000,當時很多程式員會利用這個特性編寫代碼,但是到了80286時代,處理器有了24根地址線,為了保證之前編寫的程式還能運行在80286機子上。設計人員預設關閉了A20,需要我們自己打開,這樣就解決了相容性問題。接著往下看:

  lgdt    gdtdesc
  movl    %cr0, %eax
  orl     $CR0_PE_ON, %eax
  movl    %eax, %cr0

lgdt這條指令的格式是lgdt m48操作數是一個48位的記憶體區域,該指令將這6位元組載入到全局描述表寄存器(GDTR)中,低16位是全局描述符表(GDT)的界限值,高32位是GDT的基地址。”gdtdesc“被定義在第82行:

gdt:
  SEG_NULL              # null seg
  SEG(STA_X|STA_R, 0x0, 0xffffffff) # code seg
  SEG(STA_W, 0x0, 0xffffffff)           # data seg

gdtdesc:
  .word   0x17                            # sizeof(gdt) - 1
  .long   gdt                             # address gdt

可以看到GDT有3項,第一項時空項,第二第三項分別是代碼段,數據段,它們的起始地址都是0x0,段界限都是0xffffffff。lgdt指令後面的三行是將CR0寄存器第一位置為1,其他位保持不變,這將導致處理器的運行變成保護模式。支持處理器已經進入保護模式。保護模式有疑問的同學可以參考《x86彙編語言-從實模式到保護模式》的第10,11章。

  # Set up the stack pointer and call into C.
  movl    $start, %esp
  call bootmain

接下來的分別設置esp,然後調用bootmain函數,該函數定義在/boot/main.c中。接著bootmain函數:

    struct Proghdr *ph, *eph;

    // read 1st page off disk
    readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);

    // is this a valid ELF?
    if (ELFHDR->e_magic != ELF_MAGIC)
        goto bad;

    // load each program segment (ignores ph flags)
    ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
    eph = ph + ELFHDR->e_phnum;
    for (; ph < eph; ph++)
        // p_pa is the load address of this segment (as well
        // as the physical address)
        readseg(ph->p_pa, ph->p_memsz, ph->p_offset);

    // call the entry point from the ELF header
    // note: does not return!
    ((void (*)(void)) (ELFHDR->e_entry))();

void readseg(uint32_t pa, uint32_t count, uint32_t offset)函數從磁碟offset位元組(offset相對於第一個扇區第一個位元組開始算)對應的扇區開始讀取count位元組到物理記憶體pa處。首先讀取第一個扇區的SECTSIZE*8(一頁)位元組的內核文件(ELF格式)到物理記憶體ELFHDR(0x10000)處。接下來檢查ELF文件的魔數。如果對ELF文件格式不熟悉可以看我之前的文章ELF格式。接下來從ELF文件頭讀取ELF Header的e_phoff和e_phnum欄位,分別表示Segment結構在ELF文件中的偏移,和項數。然後將每一個Segment從ph->p_offset對應的扇區讀到物理記憶體ph->p_pa處。
將內核ELF文件中的Segment從磁碟全部讀取到記憶體後,跳轉到ELFHDR->e_entry指向的指令處。正式進入內核代碼中。
這一步執行完後CPU,記憶體,磁碟可以抽象出下麵的圖:

Part 3: The Kernel

該部分將進入內核執行,主要講三件事:

  1. 開啟分頁模式,將虛擬地址[0, 4MB)映射到物理地址[0, 4MB),[0xF0000000, 0xF0000000+4MB)映射到[0, 4MB)
  2. 提供輸出格式化字元串到控制台的能力
  3. 函數的調用過程

開啟分頁模式

操作系統經常被載入到高虛擬地址處,比如0xf0100000,但是並不是所有機器都有這麼大的物理記憶體。可以使用記憶體管理硬體做到將高地址虛擬地址映射到低地址物理記憶體。虛擬地址轉換為物理地址的過程可用下麵的圖描述:

虛擬地址的高10位(0000000010B)作為頁目錄的下標,從頁目錄中獲取頁表的物理地址0x08001000,虛擬地址的第11~20位(0000000001B)作為頁表的下標,得到該頁對應的物理地址0x0000c000,最後將虛擬地址的低12位(000001010000B或者0x50)和得到的頁的物理地址(0x0000c000)加得到0x00000c050就是虛擬地址0x00801050轉換後的物理地址。
來看/kern/entry.S:

    movl    $(RELOC(entry_pgdir)), %eax
    movl    %eax, %cr3          //cr3 寄存器保存頁目錄表的物理基地址
    # Turn on paging.
    movl    %cr0, %eax
    orl $(CR0_PE|CR0_PG|CR0_WP), %eax
    movl    %eax, %cr0          //cr0 的最高位PG位設置為1後,正式打開分頁功能

第1行將$(RELOC(entry_pgdir))的值賦給eax寄存器,entry_pgdir定義在/kern/entrypgdir.c中,是頁目錄的數據結構,將虛擬地址[0, 4MB)映射到物理地址[0, 4MB),[0xF0000000, 0xF0000000+4MB)映射到[0, 4MB)

__attribute__((__aligned__(PGSIZE)))        //強制編譯器分配給entry_pgdir的空間地址是4096(一頁大小)對齊的
pde_t entry_pgdir[NPDENTRIES] = {           //頁目錄表。這是uint32_t類型長度為1024的數組
    // Map VA's [0, 4MB) to PA's [0, 4MB)
    [0]
        = ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P,    //設置頁目錄表的第0項
    // Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB)
    [KERNBASE>>PDXSHIFT]
        = ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P + PTE_W //設置頁目錄表的第KERNBASE>>PDXSHIFT(0xF0000000>>22)項
};

但是為什麼要RELOC(entry_pgdir)呢?RELOC這個巨集的定義如下:#define RELOC(x) ((x) - KERNBASE) KERNBASE又被定義在/inc/memlayout.h中#define KERNBASE 0xF0000000。那為什麼要減0xF0000000呢?因為現在還沒開啟分頁模式,entry_pgdir這個符號代表的地址又是以0xF0000000為基址的(為什麼?沒有為什麼,這個是在鏈接時,鏈接器根據/kern/kernel.ld中的. = 0xF0100000;來指定的。可以參考《程式員的自我修養》p127-使用ld鏈接腳本)。總結來說就是etnry_pgdir結構所在的物理記憶體在RELOC(entry_pgdir)處。接下來將頁目錄的物理地址複製到cr3寄存器,並且將cr0 的最高位PG位設置為1後,正式打開分頁功能。

格式化輸出到控制的台

這一小結提供了一些函數,用於將字元串輸出到控制台。我們需要瞭解這些函數的原理,並且正式開始動手寫代碼。這些函數分佈在kern/printf.c, lib/printfmt.c, kern/console.c中。閱讀總結出如下的調用關係:

void
cputchar(int c)
{
    cons_putc(c);
}
static void
cons_putc(int c)
{
    serial_putc(c);
    lpt_putc(c);
    cga_putc(c);
}
static void
cga_putc(int c)
{
    // if no attribute given, then use black on white
    if (!(c & ~0xFF))
        c |= 0x0700;

    switch (c & 0xff) {
    case '\b':
        if (crt_pos > 0) {
            crt_pos--;
            crt_buf[crt_pos] = (c & ~0xff) | ' ';
        }
        break;
    case '\n':                  //如果遇到的是換行符,將游標位置下移一行,也就是加上80(每一行占80個游標位置)
        crt_pos += CRT_COLS;
        /* fallthru */
    case '\r':                  //如果遇到的是回車符,將游標移到當前行的開頭,也就是crt_post-crt_post%80
        crt_pos -= (crt_pos % CRT_COLS);
        break;
    case '\t':                  //製表符很顯然
        cons_putc(' ');
        cons_putc(' ');
        cons_putc(' ');
        cons_putc(' ');
        cons_putc(' ');
        break;
    default:                    //普通字元的情況,直接將ascii碼填到顯存中
        crt_buf[crt_pos++] = c;     /* write the character */
        break;
    }

    // What is the purpose of this?
    if (crt_pos >= CRT_SIZE) {      //判斷是否需要滾屏。文本模式下一頁屏幕最多顯示25*80個字元,
        int i;                      //超出時,需要將2~25行往上提一行,最後一行用黑底白字的空白塊填充

        memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
        for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
            crt_buf[i] = 0x0700 | ' ';
        crt_pos -= CRT_COLS;
    }

    /* move that little blinky thing */     //移動游標
    outb(addr_6845, 14);
    outb(addr_6845 + 1, crt_pos >> 8);
    outb(addr_6845, 15);
    outb(addr_6845 + 1, crt_pos);
}

cputchar()最終會調到cga_putc(),該函數將int c列印到控制台,可以看到該函數處理會列印正常的字元外,還能處理回車換行等控制字元,甚至還能處理滾屏,具體看註釋。
根據函數調用圖,可以發現真正實現字元串輸出的是vprintfmt()函數,其他函數都是對它的包裝。vprintfmt()函數很長,大的框架是一個while迴圈,while迴圈中首先會處理常規字元:

        while ((ch = *(unsigned char *) fmt++) != '%') {        //先將非格式化字元輸出到控制台。
            if (ch == '\0')                                     //如果沒有格式化字元直接返回
                return;
            putch(ch, putdat);
        }

對於格式化的處理使用switch語句。不難理解。
看下Exercise 8,要求添加一些代碼,使能支持"%o"輸出八進位。那就很簡單了,在vprintfmt()中找到case 'o'的地方:
補充如下代碼:

            // 從ap指向的可變字元串中獲取輸出的值
            num = getuint(&ap, lflag);
            //設置基數為8
            base = 8;
            goto number;

非常容易理解,getuint函數從ap指向的可變字元串中獲取要輸出的值,將基數設置為8就行了。保存後,重新make,然後執行./grade-lab1查看當前實驗是否通過。在我的機子上顯示如下:

可以看到printf後顯示ok,說明我們通過了該實驗。

gcc函數調用過程可以用如下圖解釋:

  1. 執行call指令前,函數調用者將參數入棧,按照函數列表從右到左的順序入棧
  2. call指令會自動將當前eip入棧,ret指令將自動從棧中彈出該值到eip
  3. 被調用函數負責:將ebp入棧,esp的值賦給ebp

直接看Exercise 11,讓我們補全mon_backtrace()函數,該函數列印函數調用棧列印格式如下:

Stack backtrace:
  ebp f0109e58  eip f0100a62  args 00000001 f0109e80 f0109e98 f0100ed2 00000031
  ebp f0109ed8  eip f01000d6  args 00000000 00000000 f0100058 f0109f28 00000061
  ...

mon_backtrace()定義在/kern/monitor.c中,在/kern/init.c中被test_backtrace()調用,進入內核後會調用test_backtrace()

test_backtrace(int x)
{
    cprintf("entering test_backtrace %d\n", x);
    if (x > 0)
        test_backtrace(x-1);
    else
        mon_backtrace(0, 0, 0);
    cprintf("leaving test_backtrace %d\n", x);
}

test_backtrace(5);調用後會進行遞歸,最終調用mon_backtrace,mon_backtrace的任務就是將遞歸調用過程中的棧信息列印出來。結合之前的知識,我們可以畫出函數調用過程中ebp的值存儲圖:

至於為什麼一開始ebp的值是0?看kern/entry.S中如下代碼:

    # Clear the frame pointer register (EBP)
    # so that once we get into debugging C code,
    # stack backtraces will be terminated properly.
    movl    $0x0,%ebp           # nuke frame pointer

    # Set the stack pointer
    movl    $(bootstacktop),%esp
    # now to C code
    call    i386_init

在跳轉到i386_init函數前,已經將ebp寄存器設置為0了。現在就簡單了,開始動手實現mon_backtrace函數。
實驗提供了read_ebp()函數,可以讓我們方便獲取寄存器ebp的值。我們如下實現mon_backtrace函數。

int 
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
    // Your code here.
    uint32_t *ebp = (uint32_t *)read_ebp(); //獲取ebp的值
    while (ebp != 0) {                      //終止條件是ebp為0
        //列印ebp, eip, 最近的五個參數
        uint32_t eip = *(ebp + 1);
        cprintf("ebp %08x eip %08x args %08x %08x %08x %08x %08x\n", ebp, eip, *(ebp + 2), *(ebp + 3), *(ebp + 4), *(ebp + 5), *(ebp + 6));
        //更新ebp
        ebp = (uint32_t *)(*ebp);
    }
    return 0;
}

接著看Exercise 12,該實驗要求我們在實驗11的基礎上還要輸出當前eip(也就是當前正在執行的指令)對應的文件名,所在行號,對應函數,以及在函數內的偏移。
實驗提供了int debuginfo_eip(uintptr_t addr, struct Eipdebuginfo *info)函數(在/kern/kdebug.c中),該函數輸入eip,和一個Eipdebuginfo結構指針,執行完畢後,會將eip對應的信息填充到該結構中。接著完善mon_backtrace函數:

int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
    // Your code here.
    uint32_t *ebp = (uint32_t *)read_ebp();
    struct Eipdebuginfo eipdebuginfo;
    while (ebp != 0) {
        //列印ebp, eip, 最近的五個參數
        uint32_t eip = *(ebp + 1);
        cprintf("ebp %08x eip %08x args %08x %08x %08x %08x %08x\n", ebp, eip, *(ebp + 2), *(ebp + 3), *(ebp + 4), *(ebp + 5), *(ebp + 6));
        //列印文件名等信息
        debuginfo_eip((uintptr_t)eip, &eipdebuginfo);
        cprintf("%s:%d", eipdebuginfo.eip_file, eipdebuginfo.eip_line);
        cprintf(": %.*s+%d\n", eipdebuginfo.eip_fn_namelen, eipdebuginfo.eip_fn_name, eipdebuginfo.eip_fn_addr);
        //更新ebp
        ebp = (uint32_t *)(*ebp);
    }
    return 0;
}

在lab目錄下執行make, ./grade-lab1,如果一切順利將看到如下輸出:

就說明我們通過了lab1的所有實驗。

本人的實驗代碼已經上傳github,歡迎關註https://github.com/gatsbyd/mit_6.828_jos

參考資料

《x86彙編語言-從實模式到保護模式》
《程式員的自我修養》


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • /// /// 驗證 /// /// Account API賬號 /// TimeStamp 請求時間 /// Sign 所有請求參數 加密 public class AuthFilterOutside : AuthorizeAttribute { //重寫基類的驗證方式,加入我們自定義的Ticke... ...
  • 線程棧 stuck:存值類型,和引用類型的引用 先進後出,鏈表形式,連續擺放 CLR(公共語言運行庫(Common Language Runtime))啟動進程,main函數為一個線程入口 進程堆heap:存引用類型 進程中的一塊區域 IL:中間語言 對象的屬性為值類型出現在堆里,方法里的值類型,由 ...
  • 1,安裝Microsoft.AspNetCore.Mvc.Versioning NET Core Mvc中,微軟官方提供了一個可用的Api版本控制庫Microsoft.AspNetCore.Mvc.Versioning。 2,修改Startup類 這裡我們需要在Startup類的ConfigureS ...
  • 本人使用的是18款512g的macbookpro<後續簡稱mbp>,已升級最新mojave系統。 以下是我平時記錄、也是使用最多的快捷鍵,惠存。 1.切換拼音和字母 control+空格<或者直接按caps lock> 2.切換字母的大小寫dd 按住shift+字母-->大寫字母 3.撤銷 撤銷:c ...
  • 1. 輸出重定向 最基本的重定向是將命令的輸出發送到一個文件中。在bash shell中用大於號(>) ,格式如下:command > inputfile。例如:將date命令的輸出內容,保存到指定的輸出文件中。 如果文件已存在,重定向操作符會用新的文件數據覆蓋已有文件。這種情況下可以用雙大於號(> ...
  • 在 shell 編程中,常需要處理文本,這裡介紹幾個文本處理命令。 一、grep 命令 grep 命令由來已久,用 grep 命令來查找 文本十分方便。在 POSIX 系統上,grep 可以在兩種正則表達式風格中選擇一種(BRE 和 ERE),或是執行簡單的字元串匹配。傳統上,有三種程式可以用來查找 ...
  • SSH 包含3個組件 (1) ssh 遠程登錄節點 : ssh 用戶名@IP地址 ① 不允許空密碼或錯誤密碼認證登錄 ② 不允許root用戶登錄 ③ 有兩個版本 ssh,ssh2安全性更高 (2) sftp 文件共用連接 , xftp連接就是 sftp實現的 (3)scp 文件拷貝共用 scp命令 ...
  • 輕量桌面Archlinux用戶逃離systemd,擁抱Gentoo的openrc. 鏡像源:官方鏡像源非常慢,曾經一度體驗artix後就放棄了,後來發現了清華和騰訊雲的鏡像,速度非常快,現在又重新安裝了Artix,替代Arch和Manjaro成為了使用的主力發行版。 Artix介紹: "Artix ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...