探秘 Mach-O 文件

来源:https://www.cnblogs.com/Julday/archive/2020/06/20/13168530.html
-Advertisement-
Play Games

之前負責項目的包體積優化學習了 Mach-O 文件的格式,那麼 Mach-O 究竟是怎麼樣的文件,知道它的組成之後我們又能做點什麼?本文會從 Mach-O 文件的介紹講起,再看看認識它後的一些實際應用。 Mach-O 文件格式 先讓我們看看 Mach-O 的大致構成 再使用 MachOView 一窺 ...


之前負責項目的包體積優化學習了 Mach-O 文件的格式,那麼 Mach-O 究竟是怎麼樣的文件,知道它的組成之後我們又能做點什麼?本文會從 Mach-O 文件的介紹講起,再看看認識它後的一些實際應用。

Mach-O 文件格式

先讓我們看看 Mach-O 的大致構成

 

再使用 MachOView 一窺究竟

 

結合可知 Mach-O 文件包含了三部分內容:

  • Header(頭部),指明瞭 cpu 架構、大小端序、文件類型、Load Commands 個數等一些基本信息
  • Load Commands(載入命令),正如官方的圖所示,描述了怎樣載入每個 Segment 的信息。在 Mach-O 文件中可以有多個 Segment,每個 Segment 可能包含一個或多個 Section。
  • Data(數據區),Segment 的具體數據,包含了代碼和數據等。

Headers

Mach-O 文件的頭部定義如下:

 
  • magic 標誌符 0xfeedface 是 32 位, 0xfeedfacf 是 64 位。
  • cputype 和 cpusubtype 確定 cpu 類型、平臺
  • filetype 文件類型,可執行文件、符號文件(DSYM)、內核擴展等
  • ncmds 載入 Load Commands 的數量
  • flags dyld 載入的標誌
    • MH_NOUNDEFS 目標文件沒有未定義的符號,
    • MH_DYLDLINK 目標文件是動態鏈接輸入文件,不能被再次靜態鏈接,
    • MH_SPLIT_SEGS 只讀 segments 和 可讀寫 segments 分離,
    • MH_NO_HEAP_EXECUTION 堆記憶體不可執行…

filetype 的定義有:

 

flags 的定義有:

 

簡單總結一下就是 Headers 能幫助校驗 Mach-O 合法性和定位文件的運行環境。

Load Commands

Headers 之後就是 Load Commands,其占用的記憶體和載入命令的總數在 Headers 中已經指出。

 

 

Load Commands 的定義比較簡單:

 
  • cmd 欄位,如上圖它指出了 command 類型
    • LC_SEGMENT、LC_SEGMENT_64 將 segment 映射到進程的記憶體空間,
    • LC_UUID 二進位文件 id,與符號表 uuid 對應,可用作符號表匹配,
    • LC_LOAD_DYLINKER 啟動動態載入器,
    • LC_SYMTAB 描述在 __LINKEDIT 段的哪找字元串表、符號表,
    • LC_CODE_SIGNATURE 代碼簽名等
  • cmdsize 欄位,主要用以計算出到下一個 command 的偏移量。

Segment & Section

這裡先來看看 segment 的定義:

 
  • cmd 就是上面分析的 command 類型
  • segname 在源碼中定義的巨集
    • #define SEG_PAGEZERO "__PAGEZERO" // 可執行文件捕獲空指針的段
    • #define SEG_TEXT "__TEXT" // 代碼段,只讀數據段
    • #define SEG_DATA "__DATA" // 數據段
    • #define SEG_LINKEDIT "__LINKEDIT" // 包含動態鏈接器所需的符號、字元串表等數據
  • vmaddr 段的虛存地址(未偏移),由於 ALSR,程式會在進程加上一段偏移量(slide),真實的地址 = vm address + slide
  • vmsize 段的虛存大小
  • fileoff 段在文件的偏移
  • filesize 段在文件的大小
  • nsects 段中有多少個 section

接著看看 section 的定義:

 

__Text 和 __Data 都有自己的 section

  • segname 就是所在段的名稱
  • sectname section名稱,部分列舉:
    • Text.__text 主程式代碼
    • Text.__cstring c 字元串
    • Text.__stubs 樁代碼
    • Text.__stub_helper
    • Data.__data 初始化可變的數據
    • Data.__objc_imageinfo 鏡像信息 ,在運行時初始化時 objc_init,調用 load_images 載入新的鏡像到 infolist 中  
*   `Data.__la_symbol_ptr`
*   `Data.__nl_symbol_ptr`
*   `Data.__objc_classlist` 類列表
*   `Data.__objc_classrefs` 引用的類

這節最後探究下 stubs,在 Xcode 中新建 C 項目,代碼如下:

#include <stdio.h>
int main(int argc, const char * argv[]) {
    printf("Hello, coder\n");
    return 0;
}

使用 gcc -c main.c 將其編譯成 a.out 文件,調用 nm 命令查看 .o 文件的符號

 

看到 _printf 是未定義的,也就是說並沒有該函數的記憶體地址。nm 列印出的信息表明dyld_stub_binder 也是未定義的。 打開 Hopper 查看 .o 文件

 

可以看出 printf 會跳入 __stubs 中,地址也與 MachOView 看到的相對應

 

雙擊剛纔 __stubs 中的地址,會跳轉到 __la_symbol_ptr

 

在 MachOView 中查看 0x100001010 對應的數據為 0x10000f9c

 

用 Hopper 搜索 0x10000f9c,跳轉到 stub_helper,可知 __la_symbol_ptr 里的數據被 bind 成了 stub_helper

 

由此可知,__la_symbol_ptr 中的數據被第一次調用時會通過 dyld_stub_binder 進行相關綁定,而 __nl_symbol_ptr 中的數據就是在動態庫綁定時進行載入。

 

所以 __la_symbol_ptr 中的數據在初始狀態都被 bind 成 stub_helper,接著 dyld_stub_binder 會載入相應的動態鏈接庫,執行具體的函數實現,此時 __la_symbol_ptr 也獲取到了函數的真實地址,完成了一次近似懶載入的過程。

寫到這裡,算是快速過了一遍 Mach-O 文件的基本概念,接著聊聊可以怎樣減少項目的體積。

減少包大小

iOS 的包主要由可執行文件、資源文件(圖片)等文件組成,所以可以從這兩大頭文件入手優化。

可執行文件瘦身

我們的項目中難免會存在一些沒使用的類或方法,由於 OC 的動態特性,編譯器會對所有的源文件進行編譯,找出並刪除沒用到的類或方法可以減少可執行文件大小。 上文中提到了 __objc_classlist 和 __objc_classrefs,它們分別表示項目中全部類列表和項目中被引用的類列表,那麼取兩者之差,就能刪除一些項目中沒使用的類文件。但是在刪除過程中記住要在項目中全局搜索確認下,看看有沒有通過字元串調用無引用的類的方法,原因還是 OC 是動態語言。 在看具體做法之前,順帶提一下我公司的項目組成。我們維護著倆客戶端,共用著一個基礎庫(lib 庫),可能有時由於產品的需求變更或者為了產品功能的預留導致 lib 庫中只有著某個端使用的代碼,我在上述的做法中對腳本做了稍微改進,以防刪除了 lib 庫的代碼,導致另一個端跑不起來,下麵介紹通用的做法:

  • 在控制台輸入 otool -v -s __objc_classlist 和 otool -v -s __objc_classrefs 命令,逆向 __DATA. __objc_classlist 段和 __DATA. __objc_classrefs 段獲取當前所有oc類和被引用的oc類。
  • 取兩者差集,得到沒被引用的類的段地址
  • otool -o 二進位文件,獲取段信息
  • 通過腳本使用沒被引用的類的段地址去段信息中匹配出具體類名

壓縮圖片資源

這點就跟本文的主題沒什麼關係,不感興趣可以略過。 壓縮 app 中的圖片是我做的另一個努力,雖然 Xcode 會壓一遍,但是經我壓縮後打包發現包還是會少個將近 1m,這裡用到的工具是 ImageOptim,貼出我的三腳貓 python:

all_file_size = 0
all_file_count = 0

def fileDriector(filePath):
    global all_file_size, all_file_count

    for file in os.listdir(filePath):
        if os.path.isdir(filePath + '/' + file):
            if file != 'Pods' and not file.startswith('.') and not file.endswith('.framework') \
                    and not file.endswith('.bundle') and not file.endswith('.a') and file != 'libs' \
                    or file.endswith('.xcassets') or file.endswith('.imageset'):
                the_path = filePath + '/' + file
                fileDriector(the_path)
        elif file.endswith('.png') or file.endswith('.jpg'):
            fileName = filePath + '/' + file

            comand_line = "echo %s | imageoptim" % fileName
            test = subprocess.Popen(comand_line, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
            output = test.communicate()[0]

            numberList = re.findall('\.?\d+\.?\d*kb', output)
            lastSize = numberList[-1]

            lastSizeList = re.findall('\.?\d+\.?\d*', lastSize)
            saveSize = lastSizeList[0]
            if saveSize.startswith('.'):
                saveSize = '0' + saveSize

            finalSize = float(saveSize)
            all_file_size += finalSize
            all_file_count += 1
            print output

其他的一些減包方案就不展開了,接下來我試著分析一下 bestswifter 大神的 BSBacktraceLogger

獲取調用堆棧

 

 

說到調用堆棧,我們很容易聯想到 DSYM 文件,我們知道 Xcode build setting 有個 DEBUG INFOMATION FORMAT 的選項

 

可以看到 Debug 模式下,符號表文件會存入可執行文件中,而 Release 模式則會生成出 DSYM 文件,我們平常使用 Bugly 等工具上傳的就是這份 DSYM 文件,DSYM 也是種 Mach-O 文件。在 Debug 模式,由於符號表在記憶體中,這為我們符號化堆棧提供了可能性。

bool bs_fillThreadStateIntoMachineContext(thread_t thread, _STRUCT_MCONTEXT *machineContext) {
    mach_msg_type_number_t state_count = BS_THREAD_STATE_COUNT;
    kern_return_t kr = thread_get_state(thread, BS_THREAD_STATE, (thread_state_t)&machineContext->__ss, &state_count);
    return (kr == KERN_SUCCESS);
}

thread_get_state 函數獲取線程執行狀態(例如寄存器),傳入 _STRUCT_MCONTEXT 結構體,_STRUCT_MCONTEXT 在不同的 cpu 架構會有所不同。

uintptr_t bs_mach_instructionAddress(mcontext_t const machineContext){
    return machineContext->__ss.BS_INSTRUCTION_ADDRESS;
}

const uintptr_t instructionAddress = bs_mach_instructionAddress(&machineContext);

獲取當前指令的地址,也就是當前的棧幀,即當前被調用的函數。下麵先講下關於棧幀的概念。

棧幀是什麼

 

如上圖,一個函數調用棧是由若幹個棧幀組成,每個棧幀通過 FP 和 SP 劃分界線,fun1 函數 SP 和 FP 的指向就是 main 函數的棧幀。所以說只要知道當前函數的棧幀就能獲取上一個函數的棧幀,從而回溯出函數調用棧。

程式計數器(PC)作用是給出將要執行的下一條指令在記憶體中的地址,上面代碼的 BS_INSTRUCTION_ADDRESS。其中 16 位為 %ip,32 位為 %eip,64 位為 %rip,arm 是 pc。

SP 是棧指針寄存器,指向棧頂。

FP 是棧基址寄存器,指向棧起始位置。

LR 寄存器在子程式調用時會存儲 PC 的值,即返回值。

為了方便獲取棧幀,乾脆構造一個棧幀的結構體,以下代碼來自 KSCrash,它的註釋已經很好的講明瞭結構體的原由,BSBacktraceLogger 與之類似。

/** Represents an entry in a frame list.
 * This is modeled after the various i386/x64 frame walkers in the xnu source,
 * and seems to work fine in ARM as well. I haven't included the args pointer
 * since it's not needed in this context.
 */
typedef struct FrameEntry
{
    /** The previous frame in the list. */
    struct FrameEntry* previous;

    /** The instruction address. */
    uintptr_t return_address;
} FrameEntry;

之後,遞歸獲取函數棧幀

for(; i < 50; i++) {
    backtraceBuffer[i] = frame.return_address;
    if(backtraceBuffer[i] == 0 ||
        frame.previous == 0 ||
         bs_mach_copyMem(frame.previous, &frame, sizeof(frame)) != KERN_SUCCESS) {
        break;
     }
}

符號化

符號化地址的大致思路分三步:1. 獲取地址所在的記憶體鏡像;2. 定位到記憶體鏡像的符號表;3. 再從符號表中找到目標地址的符號。

找到地址所在的記憶體鏡像
uint32_t bs_imageIndexContainingAddress(const uintptr_t address) {
    const uint32_t imageCount = _dyld_image_count();
    const struct mach_header* header = 0;

    for(uint32_t iImg = 0; iImg < imageCount; iImg++) {
        header = _dyld_get_image_header(iImg);

遍歷 image,得到指向 image header 的指針

uintptr_t addressWSlide = address - (uintptr_t)_dyld_get_image_vmaddr_slide(iImg);
uintptr_t cmdPtr = bs_firstCmdAfterHeader(header);

對指針 +1 操作,返回指向 load command 的指針

for(uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {
    const struct load_command* loadCmd = (struct load_command*)cmdPtr;
    if(loadCmd->cmd == LC_SEGMENT) {
        const struct segment_command* segCmd = (struct segment_command*)cmdPtr;
        if(addressWSlide >= segCmd->vmaddr &&
            addressWSlide < segCmd->vmaddr + segCmd->vmsize) {
             return iImg;
      }
}

如果某個 segment 包含這個地址,那麼該地址應大於 segment 的起始地址,小於 segment 的起始地址 + segment 的大小。

定位鏡像的符號表

__LINKEDIT 段包含了符號表(symbol),字元串表(string),重定位表(relocation)。LC_SYMTAB 指明瞭 __LINKEDIT 段查找字元串和符號表的位置。我們可以結合 SEG_LINKEDIT 和 LC_SYMTAB 來找到 image 的符號表。 接下來看看段基址的獲取: 虛擬地址偏移量 = 虛擬地址(vmaddr) - 文件偏移量(fileoff) 段基址 = 虛擬地址偏移量 + ASLR的偏移量

const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx);
    // ALSR
const uintptr_t addressWithSlide = address - imageVMAddrSlide;
const uintptr_t segmentBase = bs_segmentBaseOfImageIndex(idx) + imageVMAddrSlide;
有了段基址,獲取符號表和字元串表就只是計算下 symoff 和 stroff 偏移量了:
const BS_NLIST* symbolTable = (BS_NLIST*)(segmentBase + symtabCmd->symoff);
const uintptr_t stringTable = segmentBase + symtabCmd->stroff;

找到最匹配的符號

遞歸查找離 addressWithSlide 更近的函數入口地址,因為 addressWithSlide 肯定大於某個函數的入口。

for(uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++) {
    // If n_value is 0, the symbol refers to an external object.
    if(symbolTable[iSym].n_value != 0) {
        uintptr_t symbolBase = symbolTable[iSym].n_value;
        uintptr_t currentDistance = addressWithSlide - symbolBase;
        if((addressWithSlide >= symbolBase) &&
            (currentDistance <= bestDistance)) {
             bestMatch = symbolTable + iSym;
              bestDistance = currentDistance;
            }
    }
}

如何用 MachO 文件關聯類的方法名

MachO 文件的 __Text 段有 __objc_classname 和 __objc_methname 來表示類名和方法名,但是這兩者之間是如何做到關聯的呢?下麵我以系統的計算器做例子,試著進一步研究下 MachO 文件。 使用 MachOView 打開系統電腦,先來看看 __objc_classname 和 __objc_methname 在 load commands 里的定義:

   

我們順著 __objc_classname 的偏移offset 109518 即 0x1ABCE 來到:

 

同理 __objc_methname 的偏移為 0x165E8:

 

那麼,怎樣像 class-dump 那樣將類和自個的方法名對應起來呢? 由於每個類的虛擬地址都在Data 段 __objc_classlist 中:

 

我們看到起始地址對應的是 0x1000298A8 這個地址,為了得到實際的地址需要用虛擬地址 - 段起始地址 + 文件偏移,經過一番計算,結果是0x298A8,來到文件偏移處,已經在DATA 段的 __objc_data

 

在這裡會對應著類的結構體,代碼拷自 class-dump

    struct cd_objc2_class {
        uint64_t isa;
        uint64_t superclass;
        uint64_t cache;
        uint64_t vtable;
        uint64_t data; // points to class_ro_t
        uint64_t reserved1;
        uint64_t reserved2;
        uint64_t reserved3;
    };

data 是我們感興趣的,它指向 class_ro_t,熟悉 runtime 的話應該知道 class_ro_t 存儲了類在編譯器就確定的屬性、方法、協議等。 所以上圖 isa 的數據是 0x1000298D0,繼續順著找下去 0x100020A68 就是 data 的記憶體地址,再用上面的公式計算得到 0x20A68,我們在 __objc_const找到那裡:

 

這裡就是對應著 class_ro_t,來看看它在 class-dump 里的定義:

    struct cd_objc2_class_ro_t {
        uint32_t flags;
        uint32_t instanceStart;
        uint32_t instanceSize;
        uint32_t reserved; // *** this field does not exist in the 32-bit version ***
        uint64_t ivarLayout;
        uint64_t name;
        uint64_t baseMethods;
        uint64_t baseProtocols;
        uint64_t ivars;
        uint64_t weakIvarLayout;
        uint64_t baseProperties;
    };

最終 0x20A80 就是name,0x20A88 就是 baseMethods。name 對應的正好是 0x1ABCE,類名是 BitFieldBox。baseMethods 指向記憶體 0x100020A00,該地址對應的數據是 18 00 00 00 04 00 00 00 表示 entsize 和 count 方法數,在這8個位元組之後就是 name 方法名,types 方法類型, imp 函數指針了,所以方法名處的數據為 0x1000165e8 剛好對應 initWithFrame: 將結論用 class-dump 驗證可得 BitFieldBox 的第一個方法是 initWithFrame

 

總結

最初學習 MachO 文件格式覺得挺抽象的,後來經過各種源碼的閱讀和融合,終於在一次次地探索中比較直觀地認識了 MachO 文件,特別是在 MachO 文件關聯類的方法名時對類在記憶體中的佈局有了更進一步的認識。雖然我們平常開發基本不和 MachO 文件打交道,但是對它有個基本概念,無論是做崩潰分析、逆向等都是有幫助的。

面試資料:

面試題持續整理更新中,如果你想一起進階去大廠,不妨添加一下交流群1012951431

面試題資料或者相關學習資料都在群文件中 進群即可下載!

 

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

-Advertisement-
Play Games
更多相關文章
  • ffmpeg -i test.mp4 -codec copy -bsf: h264_mp4toannexb -f h264 test.264 從MP4文件內提取視頻流,忽略音頻流,指定幀頻、碼率 ffmpeg -i test.mp4 -vcodec h264 -an -r 25 -b:v 256k ...
  • 初用MySQL Mysql示例庫 Navicat15 查看初始密碼 MySQl首次啟動會創建“超級管理員賬號”root@localhost,初始密碼存儲在日誌文件中,通過grep搜索並查看: grep 'temporary password' /var/log/mysqld.log 進入mysql ...
  • 對於MySQL的一些個規範,某些公司建表規範中有一項要求是所有欄位非空,意味著沒有值的時候存儲一個預設值。其實所有欄位非空這麼說應該是絕對了,應該說是儘可能非空,某些情況下不可能給出一個預設值。那麼這條要求,是基於哪些考慮因素,存儲空間?相關增刪查改操作的性能?亦或是其他考慮?該理論到底有沒有道理或 ...
  • 1. 安裝資料庫 1) yum -y install mysql-server(簡單) yum命令自動從網上尋找mysql服務資源,下載至本地並完成安裝 2) 也可以自己在網上下載mysql服務,通過xftp傳輸至Linux系統,自己安裝(一般安裝在usr或opt目錄下) 2. 啟動資料庫 安裝完畢 ...
  • MySQL中給一張千萬甚至更大量級的表添加欄位一直是比較頭疼的問題,遇到此情況通常該如果處理?本文通過常見的三種場景進行案例說明。 1、 環境準備 資料庫版本: 5.7.25-28(Percona 分支) 伺服器配置: 3台centos 7虛擬機,配置均為2CPU 2G記憶體 資料庫架構: 1主2從的 ...
  • 一、我們創建一個新的android項目來進行演示廣播機制中是如何​顯示網路狀態的。 package com.example.broadcasttest2; import android.app.Activity; import android.content.BroadcastReceiver; i ...
  • 一、block記憶體管理 1.block記憶體類型 block記憶體分為三種類型: _NSConcreteGlobalBlock(全局) _NSConcreteStackBlock(棧) _NSConcreteMallocBlock(堆) 2.三種類型的記憶體的創建時機 1)對於_NSConcreteSta ...
  • Xcode 中的調試技巧與我們的日常開發息息相關,而這些調試技巧在我們解決Bug時,常常有事半功倍的作用,經常會用到的有各種斷點 和 命令。而這些調試技巧也經常會在面試中問到,所以不知道的就來看看吧。 調試主要觀看區 調試命令 在上圖中,右側綠色區域就是Log 輸出區,在 Log 輸出區可以使用一些 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...