自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

程序員內(nèi)功修煉:五分鐘徹底搞懂 Linux ELF 文件 !

系統(tǒng)
ELF 文件不僅僅是一個(gè)格式,它是 Linux 世界中程序的"靈魂容器",承載著程序從編譯到執(zhí)行的整個(gè)生命周期。?

大家好啊,我是小康。

今天咱們來聊一個(gè)看起來高深,實(shí)際上理解起來其實(shí)挺簡單的話題—— ELF 文件。

不知道你有沒有想過:我們敲下./program命令的那一刻,計(jì)算機(jī)是怎么把這個(gè)文件變成一個(gè)活蹦亂跳的進(jìn)程的?這背后的"黑魔法"到底是什么?

沒錯(cuò),答案就是今天的主角:ELF(Executable and Linkable Format)可執(zhí)行與可鏈接格式。你可以把它理解為 Linux 世界里程序的"靈魂容器"!

一、什么是 ELF 文件?給個(gè)痛快話!

簡單來說,ELF 是 Linux 下的可執(zhí)行文件格式,就像 Windows 下的 .exe 一樣。但別被這個(gè)簡單的解釋騙了,ELF 可比 .exe 復(fù)雜得多,也強(qiáng)大得多!

ELF 文件可以是:

  • 可執(zhí)行文件(比如你的./program)
  • 目標(biāo)文件(編譯后但還沒鏈接的 .o 文件)
  • 共享庫文件(就是 .so 文件,類似 Windows 下的 .dll)
  • 核心轉(zhuǎn)儲(chǔ)文件(程序崩潰時(shí)的那個(gè)core dump)

本質(zhì)上,ELF 就是一個(gè)容器,里面裝著代碼、數(shù)據(jù)以及程序運(yùn)行所需的各種信息,按照特定的格式組織起來。

二、初見 ELF:第一印象很重要

想知道一個(gè)文件是不是 ELF 格式的?超簡單:

$ file /bin/ls
/bin/ls: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, ...

看到?jīng)]?只要文件輸出信息的開頭是"ELF",那它就是 ELF 格式的!

再來點(diǎn)兒硬核的,我們直接看一下 ELF 文件的前幾個(gè)字節(jié):

$ hexdump -C -n 16 /bin/ls
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|

這里最開始的7f 45 4c 46就是 ELF 文件的"魔數(shù)"(Magic Number)。其中 45 4c 46 是 ASCII 碼中的 "ELF" 三個(gè)字母,前面的 7f 是一個(gè)特殊字符。這四個(gè)字節(jié)就是 ELF 文件的"身份證",操作系統(tǒng)首先會(huì)檢查這四個(gè)字節(jié),確認(rèn)它是不是一個(gè) ELF 文件。

三、ELF 文件的內(nèi)部結(jié)構(gòu):化繁為簡

很多教程一上來就給你畫個(gè)復(fù)雜的結(jié)構(gòu)圖,看得人頭暈眼花。咱們先別急,我用一個(gè)簡單的類比來幫你理解:

把 ELF 文件想象成一本"程序說明書",這本書有三部分組成:

  • 文件頭(ELF Header):相當(dāng)于書的封面和目錄,告訴你這本書有什么內(nèi)容,怎么看
  • 程序頭表(Program Header Table):相當(dāng)于給"閱讀器"(操作系統(tǒng))看的指南,告訴它怎么把這本書變成一個(gè)活的程序
  • 節(jié)區(qū)頭表(Section Header Table):相當(dāng)于給"編輯器"(鏈接器、調(diào)試器)看的指南,告訴它這本書的內(nèi)部結(jié)構(gòu)

然后,書的主體內(nèi)容就是各種節(jié)區(qū)(Sections)或段(Segments),里面裝著代碼、數(shù)據(jù)等實(shí)際內(nèi)容。

直觀一點(diǎn),用圖來表示就是:

+------------------+
|    ELF Header    | <-- 文件開始處的標(biāo)識(shí)信息和總體布局
+------------------+
|     程序頭表      | <-- 告訴操作系統(tǒng)如何加載
| Program Header 1 |
| Program Header 2 |
|       ...        |
+------------------+
|    Section 1     | <-- 實(shí)際內(nèi)容,如代碼、數(shù)據(jù)等
|    Section 2     |
|       ...        |
+------------------+
|    節(jié)區(qū)頭表       |  <-- 描述每個(gè)Section的信息
| Section Header 1 | 
| Section Header 2 |
|       ...        |
+------------------+

哎,你可能會(huì)問:什么是節(jié)區(qū)(Section)?什么又是段(Segment)?它們有什么區(qū)別?

簡單來說:

  • 節(jié)區(qū)(Section):是 ELF 文件存儲(chǔ)的基本單位,針對鏈接器
  • 段(Segment):是運(yùn)行時(shí)內(nèi)存的基本單位,針對加載器

一個(gè)段通常包含多個(gè)功能相似的節(jié)區(qū)。比如,包含代碼的所有節(jié)區(qū)會(huì)被歸入到一個(gè)叫做"TEXT"的段中。

四、深入解剖 ELF文件:逐層剝開

1. ELF頭(ELF Header)

ELF 頭是整個(gè)文件的"門面",包含了文件的基本信息和指向其他部分的指針。用readelf -h命令可以查看:

$ readelf -h /bin/ls
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x5850
  Start of program headers:          64 (bytes into file)
  Start of section headers:          136912 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         13
  Size of section headers:           64 (bytes)
  Number of section headers:         30
  Section header string table index: 29

這里面最重要的信息是:

  • Entry point address:程序執(zhí)行的起點(diǎn)地址
  • Start of program headers:程序頭表的位置
  • Start of section headers:節(jié)區(qū)頭表的位置

2. 程序頭表(Program Header Table)

程序頭表告訴操作系統(tǒng)如何創(chuàng)建進(jìn)程映像,用readelf -l命令查看:

$ readelf -l /bin/ls

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x00000000000002d8 0x00000000000002d8  R      0x8
  INTERP         0x0000000000000318 0x0000000000000318 0x0000000000000318
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000004428 0x0000000000004428  R      0x1000
  LOAD           0x0000000000005000 0x0000000000005000 0x0000000000005000
                 0x0000000000012d1c 0x0000000000012d1c  R E    0x1000
  LOAD           0x0000000000018000 0x0000000000018000 0x0000000000018000
                 0x0000000000004d40 0x0000000000004d40  R      0x1000
  LOAD           0x000000000001d520 0x000000000001e520 0x000000000001e520
                 0x0000000000001640 0x0000000000002270  RW     0x1000
...

最重要的是那些類型為LOAD的段,它們會(huì)被加載到內(nèi)存中。

注意看Flags:

  • R表示可讀(Read)
  • W表示可寫(Write)
  • E表示可執(zhí)行(Execute)

這就是為什么有的內(nèi)存區(qū)域可執(zhí)行,有的只能讀不能寫,這些權(quán)限在 ELF 文件里就定義好了!

3. 節(jié)區(qū)頭表(Section Header Table)

節(jié)區(qū)頭表描述了文件中各個(gè)節(jié)區(qū)的信息,用readelf -S查看:

$ readelf -S /bin/ls
There are 30 section headers, starting at offset 0x21730:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000000318  00000318
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.gnu.build-i NOTE             0000000000000338  00000338
       0000000000000024  0000000000000000   A       0     0     4
...
  [11] .text             PROGBITS         00000000000052a0  000052a0
       0000000000012a7c  0000000000000000  AX       0     0     16
...
  [23] .data             PROGBITS         000000000001e520  0001d520
       0000000000000e60  0000000000000000  WA       0     0     32
  [24] .bss              NOBITS           000000000001f380  0001e380
       0000000000001410  0000000000000000  WA       0     0     32
...

常見的重要節(jié)區(qū)包括:

  • .text:存放程序的機(jī)器代碼
  • .data:已初始化的全局變量和靜態(tài)變量
  • .bss:未初始化的全局變量和靜態(tài)變量(不占用文件空間)
  • .rodata:只讀數(shù)據(jù)(如字符串常量)
  • .symtab:符號(hào)表,存儲(chǔ)程序中定義和引用的函數(shù)、變量
  • .strtab:字符串表,通常存儲(chǔ)符號(hào)名
  • .dynamic:動(dòng)態(tài)鏈接信息

五、ELF 文件的生命周期:從編譯到執(zhí)行

為了徹底搞懂 ELF 文件,我們需要了解它的整個(gè)生命周期:

源代碼(.c) --編譯--> 目標(biāo)文件(.o) --鏈接--> 可執(zhí)行文件 --加載--> 進(jìn)程

1. 編譯階段:生成目標(biāo)文件(.o)

當(dāng)你寫完 C 代碼,運(yùn)行g(shù)cc -c hello.c時(shí),會(huì)得到一個(gè)hello.o的目標(biāo)文件。這個(gè)文件已經(jīng)是 ELF 格式的了,但它還不能直接執(zhí)行,因?yàn)槔锩嬗泻芏?坑"等著被填上。

這些"坑"在 ELF 文件中表現(xiàn)為"重定位表",用readelf -r可以看到:

$ readelf -r hello.o

Relocation section '.rela.text' at offset 0x2d0 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000013  000a00000004 R_X86_64_PLT32    0000000000000000 printf - 4
000000000023  000b00000004 R_X86_64_PLT32    0000000000000000 exit - 4

這表示代碼中調(diào)用了printf和exit函數(shù),但編譯器不知道它們在哪兒,所以留了個(gè)"坑"等著鏈接器來填。

2. 符號(hào)表:程序的"通訊錄"

說到這些函數(shù)(printf 、exit),咱們不得不提 ELF 文件中的"符號(hào)表"。簡單來說,符號(hào)表就像是程序的"通訊錄",記錄了程序中所有函數(shù)和變量的名字和位置。

來看看符號(hào)表長啥樣:

$ readelf -s hello.o

Symbol table '.symtab' contains 12 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
    ...
     9: 0000000000000000    41 FUNC    GLOBAL DEFAULT    1 main
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
    11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND exit

瞧,這里面有main函數(shù)(我們自己定義的),還有printf和exit(外部函數(shù))。注意它們的Ndx(索引)列:main是1,表示在第1個(gè)節(jié)區(qū);而printf和exit是UND,表示"未定義",這就是前面說的"坑"。

這個(gè)目標(biāo)文件的符號(hào)表就像一張"半成品通訊錄",只記錄了自己有什么函數(shù),以及自己需要哪些外部函數(shù),但還不知道那些外部函數(shù)在哪里。所以它還不能獨(dú)立工作,需要鏈接器來幫忙找到這些外部函數(shù)。

3. 動(dòng)態(tài)鏈接:程序的"即插即用"

說到外部函數(shù),就不得不提 ELF 的一個(gè)超強(qiáng)功能:動(dòng)態(tài)鏈接。

還記得 Windows 上安裝軟件時(shí)經(jīng)常冒出的"DLL缺失"錯(cuò)誤嗎?Linux 上也有類似概念,不過實(shí)現(xiàn)得更優(yōu)雅,這就是動(dòng)態(tài)鏈接庫(.so文件)。

動(dòng)態(tài)鏈接的好處簡直不要太多:

  • 節(jié)省內(nèi)存:多個(gè)程序共享同一個(gè)庫
  • 節(jié)省磁盤:不用把所有代碼都打包進(jìn)可執(zhí)行文件
  • 方便升級:庫更新后,程序自動(dòng)用上新版本,不用重新編譯

那么問題來了:程序怎么知道自己需要哪些庫?又是如何找到這些庫的呢?

ELF 文件中有一個(gè)特殊的.dynamic節(jié)區(qū),專門記錄這些信息:

$ readelf -d /bin/ls | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: [libselinux.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]

這告訴我們,ls命令依賴于這兩個(gè)共享庫。如果你想更直觀地看到所有依賴及它們的實(shí)際位置,可以用ldd命令:

$ ldd /bin/ls
 linux-vdso.so.1 (0x00007ffc961cd000)
 libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f27f989e000)
 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f27f96b3000)
 ...

看到?jīng)]?ldd不僅告訴你需要哪些庫,還告訴你它們的實(shí)際位置和加載地址。

那程序又是怎么找到這些庫的呢?它會(huì)按照以下順序查找:

  • 環(huán)境變量LD_LIBRARY_PATH指定的目錄
  • 可執(zhí)行文件的RPATH屬性指定的目錄
  • /etc/ld.so.cache緩存中記錄的位置
  • 默認(rèn)目錄如/lib、/usr/lib等

動(dòng)態(tài)鏈接器(ld.so)會(huì)在程序啟動(dòng)時(shí)自動(dòng)處理這些依賴關(guān)系,把所有需要的庫都加載進(jìn)來,就像樂高積木一樣把程序拼裝完整,非常巧妙!

4. 鏈接階段:生成可執(zhí)行文件

鏈接器會(huì)把多個(gè)目標(biāo)文件和庫文件鏈接在一起,解決那些"坑"(重定位),最終生成可執(zhí)行文件。

那么鏈接器具體是怎么解決這些"坑"的呢?簡單來說就是做個(gè)"牽線搭橋"的活:

  • 收集所有目標(biāo)文件中的符號(hào)表,建立一個(gè)全局符號(hào)表
  • 找到所有標(biāo)記為"未定義"(UND)的符號(hào)
  • 在全局符號(hào)表或者庫文件中尋找這些符號(hào)的定義
  • 把找到的地址填回原來的"坑"中

比如當(dāng)鏈接器找到printf函數(shù)在 libc.so 中的實(shí)際地址后,就會(huì)修改原來調(diào)用 printf 的指令,讓它指向正確的地址。

鏈接完成后,再看同一個(gè)程序的符號(hào)表,會(huì)發(fā)現(xiàn)那些 UND 的符號(hào)要么有了實(shí)際地址(靜態(tài)鏈接),要么指向了動(dòng)態(tài)鏈接的跳轉(zhuǎn)表(動(dòng)態(tài)鏈接)。

在動(dòng)態(tài)鏈接的情況下,還會(huì)在 ELF 文件中記錄運(yùn)行時(shí)需要哪些共享庫,前面已經(jīng)說過了。

5. 加載階段:從文件到進(jìn)程

當(dāng)你執(zhí)行./program時(shí),操作系統(tǒng)(確切地說是加載器 ld.so )會(huì)做這些事:

  • 檢查 ELF 頭的合法性
  • 根據(jù)程序頭表,將需要的段加載到內(nèi)存
  • 如果是動(dòng)態(tài)鏈接的,還會(huì)找到并加載所需的共享庫
  • 跳轉(zhuǎn)到 Entry Point 開始執(zhí)行

這個(gè)過程可以用strace命令觀察:

$ strace ./hello
execve("./hello", ["./hello"], 0x7ffcef8db490 /* 52 vars */) = 0
brk(NULL)                               = 0x55c84f34c000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
...

execve就是創(chuàng)建新進(jìn)程的系統(tǒng)調(diào)用,后面一系列操作就是在加載和準(zhǔn)備程序運(yùn)行環(huán)境。

六、ELF實(shí)用工具箱:玩轉(zhuǎn)ELF文件

好了,了解了 ELF 的原理后,來看看有哪些工具可以幫我們操作 ELF 文件:

(1) file:判斷文件類型

$ file /bin/ls

(2) readelf:查看ELF文件的所有信息

$ readelf -a /bin/ls  # 顯示全部信息

(3) objdump:反匯編 ELF 文件

$ objdump -d /bin/ls  # 反匯編代碼段

(4) nm:列出符號(hào)表

$ nm /bin/ls  # 顯示符號(hào)(函數(shù)、變量)

(5) ldd:查看動(dòng)態(tài)依賴

$ ldd /bin/ls  # 顯示依賴的共享庫

(6) strings:提取文件中的字符串

$ strings /bin/ls | grep "GNU"  # 查找包含"GNU"的字符串

(7) strip:移除ELF文件中的符號(hào)表和調(diào)試信息

$ strip -s program  # 減小文件體積

(8) patchelf:修改 ELF 文件的屬性

$ patchelf --set-interpreter /lib64/ld-custom.so program  # 修改解釋器

七、實(shí)際應(yīng)用:ELF文件的那些神奇玩法

ELF文件的知識(shí)不僅僅是理論,來看看一些實(shí)際的例子:

1. 程序加固與混淆

想象你開發(fā)了一個(gè)軟件不想被輕易破解:

# 刪除符號(hào)表,讓逆向分析更困難
$ strip --strip-all myprogram

# 對比前后大小
$ ls -lh myprogram*
-rwxr-xr-x 1 user user 236K myprogram
-rwxr-xr-x 1 user user 176K myprogram.stripped

看,文件體積一下減少了幾十k,因?yàn)榉?hào)信息都被刪掉了!

2. 程序補(bǔ)丁與熱修復(fù)

假設(shè)你想修改程序使用的解釋器路徑:

# 查看當(dāng)前解釋器
$ readelf -l myprogram | grep interpreter
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

# 修改為自定義解釋器
$ patchelf --set-interpreter /opt/mylibs/ld-linux.so myprogram

# 確認(rèn)修改成功
$ readelf -l myprogram | grep interpreter
      [Requesting program interpreter: /opt/mylibs/ld-linux.so]

這樣程序就會(huì)使用你自定義的動(dòng)態(tài)鏈接器,而不需要重新編譯!

更酷的是,Linux 還提供了一種不用重啟程序就能熱修復(fù)的黑科技——LD_PRELOAD環(huán)境變量!它可以讓你悄悄地"替換"程序中的函數(shù)實(shí)現(xiàn)。

來看一個(gè)簡單實(shí)用的例子 —— 監(jiān)控程序的內(nèi)存分配:

創(chuàng)建一個(gè)簡單的內(nèi)存跟蹤庫: memtrace.c

#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>

// 原始malloc函數(shù)指針
staticvoid* (*real_malloc)(size_t) = NULL;

// 攔截 malloc 函數(shù)
void* malloc(size_t size) {
    // 延遲初始化原始函數(shù)
    if (real_malloc == NULL) {
        real_malloc = dlsym(RTLD_NEXT, "malloc");
    }
    
    // 調(diào)用原始malloc
    void* ptr = real_malloc(size);
    
    // 打印跟蹤信息
    fprintf(stderr, "malloc(%zu) = %p\n", size, ptr);
    
    return ptr;
}

編譯成共享庫:

$ gcc -shared -fPIC memtrace.c -o libmemtrace.so -ldl

接著使用我們的庫監(jiān)控任何程序的內(nèi)存分配:

LD_PRELOAD=./libmemtrace.so ./my_program

輸出:

malloc(100) = 0x55e930e2f6b0
malloc(200) = 0x55e930e2f720
malloc(300) = 0x55e930e2f7f0

看到了嗎?我們只用了十幾行代碼,就實(shí)現(xiàn)了一個(gè)能夠監(jiān)控任何程序內(nèi)存分配的工具!這個(gè)例子的工作原理很簡單:

  • 定義一個(gè)與系統(tǒng)函數(shù)同名的malloc
  • 用dlsym(RTLD_NEXT, "malloc")找到真正的 malloc 函數(shù)
  • 在調(diào)用真正的 malloc 前后添加我們的代碼(這里是打印日志)
  • 通過LD_PRELOAD讓系統(tǒng)優(yōu)先加載我們的庫

這種技術(shù)經(jīng)常用于:

  • 調(diào)試內(nèi)存問題
  • 給程序添加日志
  • 修改程序行為而不用改源碼
  • 臨時(shí)修復(fù)運(yùn)行中的服務(wù)

當(dāng)然,這項(xiàng)技術(shù)也常被黑客利用來劫持程序函數(shù),所以理解它不僅能提升編程能力,也對安全防護(hù)很重要!

八、總結(jié):ELF 文件的精髓

好了,咱們來總結(jié)一下 ELF 文件的核心要點(diǎn):

(1) ELF是容器:裝載了代碼、數(shù)據(jù)和各種元數(shù)據(jù)

(2) 分層結(jié)構(gòu):ELF 頭、程序頭表、節(jié)區(qū)、節(jié)區(qū)頭表

(3) 兩種視角:

  • 執(zhí)行視角:段(Segments)- 加載器關(guān)心
  • 鏈接視角:節(jié)(Sections)- 鏈接器關(guān)心

(4) 生命周期:從源代碼到目標(biāo)文件,再到可執(zhí)行文件,最后變成進(jìn)程

當(dāng)你理解了 ELF 文件的本質(zhì),Linux 下的很多問題就迎刃而解了:為什么有些程序不能在不同版本的 Linux 上運(yùn)行?為什么動(dòng)態(tài)庫版本不匹配會(huì)導(dǎo)致程序崩潰?為什么有些惡意軟件難以檢測?——這些問題的答案都藏在 ELF 文件的結(jié)構(gòu)中!

記住,ELF 文件不僅僅是一個(gè)格式,它是 Linux 世界中程序的"靈魂容器",承載著程序從編譯到執(zhí)行的整個(gè)生命周期。

責(zé)任編輯:趙寧寧 來源: 跟著小康學(xué)編程
相關(guān)推薦

2009-12-04 10:45:20

程序員職場

2021-06-18 07:34:12

Kafka中間件微服務(wù)

2024-12-11 07:00:00

面向?qū)ο?/a>代碼

2025-03-13 06:22:59

2019-08-09 10:33:36

開發(fā)技能代碼

2025-01-20 08:50:00

2025-01-21 07:39:04

Linux堆內(nèi)存Golang

2009-05-21 16:23:23

程序員法則職場

2023-09-18 15:49:40

Ingress云原生Kubernetes

2023-12-06 08:48:36

Kubernetes組件

2024-04-29 07:57:46

分布式流控算法

2022-05-23 09:10:00

分布式工具算法

2024-12-04 16:12:31

2020-03-03 19:59:38

主板無線網(wǎng)卡

2019-06-14 09:34:59

Linux 系統(tǒng) 數(shù)據(jù)

2017-03-30 19:28:26

HBase分布式數(shù)據(jù)

2018-09-27 13:56:14

內(nèi)網(wǎng)外網(wǎng)通信

2023-10-06 20:21:28

Python鏈表

2009-11-16 11:30:55

PHP上傳文件代碼

2024-12-04 10:12:06

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)