Linux內核如何啟動並裝載一個可執行程式

来源:http://www.cnblogs.com/zhangchao0515/archive/2016/04/08/5362395.html
-Advertisement-
Play Games

2016-04-07 張超《Linux內核分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000#/info 一、理解編譯鏈接的過程和ELF可執行文件格式 我給出了一個例子: 第一步:先編輯一個hello.c,如下 vi hello.c 1 ...


2016-04-07

張超《Linux內核分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000#/info

 

一、理解編譯鏈接的過程和ELF可執行文件格式

   

我給出了一個例子:

第一步:先編輯一個hello.c,如下

vi hello.c

1   #include <stdio.h>
2   #include <stdlib.h>
3   
4   int main()
5   {
6       printf("Hello World!\n");
7       return 0;
8   }
hello.c

第二步:生成預處理文件hello.cpp(預處理負責把include的文件包含進來及巨集替換等工作)

gcc -E -o hello.cpp hello.c -m32     

vi hello.cpp  (查看一下hello.cpp的內容,但是太多,這裡就不顯示了)

第三步:編譯成彙編代碼hello.s

gcc -x cpp-output -S -o hello.s hello.cpp -m32 

vi hello.s  (查看一下hello.s的內容,如下所示)

  1     .file   "hello.c"
  2     .section    .rodata
  3 .LC0:
  4     .string "Hello World!"
  5     .text
  6     .globl  main
  7     .type   main, @function
  8 main:
  9 .LFB2:
 10     .cfi_startproc
 11     pushl   %ebp
 12     .cfi_def_cfa_offset 8
 13     .cfi_offset 5, -8
 14     movl    %esp, %ebp
 15     .cfi_def_cfa_register 5
 16     andl    $-16, %esp
 17     subl    $16, %esp
 18     movl    $.LC0, (%esp)
 19     call    puts
 20     movl    $0, %eax
 21     leave
 22     .cfi_restore 5
 23     .cfi_def_cfa 4, 4
  24     ret
 25     .cfi_endproc
 26 .LFE2:
 27     .size   main, .-main
 28     .ident  "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.1) 4.8.4"
 29     .section    .note.GNU-stack,"",@progbits
                                     
hello.s

第四步:編譯成目標代碼,得到二進位文件hello.o,(該文件中有機器指令,但不能在機器上直接運行)

gcc -x assembler -c hello.s -o hello.o -m32

vi hello.o (查看一下hello.o的內容,結果是亂碼)

第五步:鏈接成可執行文件hello,(它是二進位文件)

gcc -o hello hello.o -m32

vi hello(查看一下hello,發現除了幾個英文標示符可見,其他都是亂碼文件,不過開頭有ELF)

第六步:運行一下

./hello

其實:我們可以用一個命令直接只生成可執行文件hello,並運行

gcc -o hello hello.c

這裡,hello和hello.o都是ELF格式的文件。

而這樣編譯出來的hello是使用的共用庫。

我們也可以靜態編譯,(是完全把所有需要執行所依賴的東西放到程式內部)

gcc -o hello.static hello.o -m32 -static  

hello.static 也是ELF格式文件

運行一下hello.static

./hello.static

ls -l    (查看一下各個文件)

發現hello.static (733298)比 hello (7336)大的多。

 

靜態鏈接於動態鏈接:

  1. 靜態鏈接方式於動態鏈接方式
  • 靜態鏈接方式:在程式運行之前完成所有的組裝工作,生成一個可執行的目標文件
  • 動態鏈接方式:在程式已經為了執行被裝載入記憶體之後完成鏈接工作,並且在記憶體中一般只保留該編譯單元的一份拷貝

   2.靜態鏈接庫(.a(Linux下)  .lib(Windows下))與動態鏈接庫(.so(Linux下)  .dll(Windows下))

    靜態鏈接庫與動態鏈接庫都是共用代碼的方式。如果採用靜態鏈接庫,則程式把所有執行需要所依賴的東西都載入到可執行文件中了。但若是使用動態庫,則不必被包含在最終執行的文件中,可執行文件執行時可以“動態”的引用和卸載這個與可執行程式獨立的動態庫文件。

    簡單的說,靜態庫和應用程式編譯在一起了,在任何情況下都能運行,而動態庫是動態鏈接,顧名思義就是在應用程式啟動的時候再會鏈接的,所以,當用戶的系統上沒有該動態庫時,應用程式就會運行失敗。

  3.靜態庫於動態庫的特點

    靜態庫:代碼的裝載速度快,執行速度也快,因為編譯時它只會把你所需要的那部分鏈接進去,應用程式相對較大。但是如果沒有多個應用程式的話,會被裝載多次,浪費記憶體。

    動態庫:

      共用:多個應用程式可以使用同一個動態庫,啟動多個應用程式的時候,只需要將動態庫載入到記憶體一次就好。

      開發模塊好:要求設計者對功能劃分的比較好。

  4.認識動態鏈接庫

    動態鏈接庫是相對靜態鏈接庫而言的。所謂的靜態鏈接庫只把要調用的函數或過程鏈接到可執行文件中,成為可執行文件的一部分。換句話說,函數和過程的代碼就在程式的可執行文件中,該文件包含了運行時所需的全部代碼。當多個程式都調用相同函數時,記憶體中就會存在這個函數的多個拷貝,這樣就浪費了寶貴的記憶體資源,而動態鏈接所調用的函數代碼並沒有被拷貝到應用程式的可執行文件中去,而是僅僅在其中加入了所調用函數的描述信息(往往是一些重定位信息)。僅當應用程式被裝入記憶體開始運行時,在系統的管理下,才在應用程式與相應的動態庫之間建立鏈接關係。當要執行所調用的動態鏈接庫中的函數時,根據鏈接產生的重定位信息,系統下轉去執行狀態鏈接庫中相應的函數代碼。一般情況下,如果一個應用程式使用了動態鏈接庫,系統保證記憶體只有動態庫的一份複製品。

    動態鏈接庫的兩種鏈接方法:

    1)裝載時動態鏈接(Load-time Dynamic Linking):這種方法的前提是在編譯之前已經明確知道要調用的動態庫的哪些函數,編譯時在目標文件中只保留必要的鏈接信息,而不含動態庫函數代碼;當程式執行時,調用函數的時候利用鏈接信息載入動態庫函數代碼併在記憶體中將其鏈接入調用程式的執行空間中(全部函數載入進記憶體),其主要目的是便於代碼共用。(動態載入程式,處在載入階段,主要為了共用代碼,共用代碼記憶體)

    2)運行時動態鏈接(Run-time Dynamic Linking):這種方式是指在編譯之前並不知道將會調用哪些動態庫函數,完全是在運行過程中根據需要決定應調用哪個函數,將其載入到記憶體中(只載入調用的函數進記憶體);並標識記憶體地址,其他程式也可以使用該程式,並獲得動態庫函數的入口地址。(動態庫在記憶體中只存在一份,處在運行階段)

 

要想理解一個進程是怎樣執行的,一個可執行程式是如何載入成進程的?我們這時需要知道可執行文件的內部是什麼?

目標文件及鏈接

ELF目標文件格式:ELF(Executable And Linkable Format)

  • ELF文件格式--(中文翻譯版本
     1 #define EI_NIDENT       16
     2 
     3   typedef struct {
     4       unsigned char       e_ident[EI_NIDENT];
     5       Elf32_Half          e_type;
     6       Elf32_Half          e_machine;
     7       Elf32_Word          e_version;
     8       Elf32_Addr          e_entry;
     9       Elf32_Off           e_phoff;
    10       Elf32_Off           e_shoff;
    11       Elf32_Word          e_flags;
    12       Elf32_Half          e_ehsize;
    13       Elf32_Half          e_phentsize;
    14       Elf32_Half          e_phnum;
    15       Elf32_Half          e_shentsize;
    16       Elf32_Half          e_shnum;
    17       Elf32_Half          e_shstrndx;
    18   } Elf32_Ehdr;
    ELF Header
  • 查看ELF文件的頭部(hello)

   readlf -h hello

   

  • 查看該ELF所依賴的共用庫(過程及結果如下圖所示)

   ldd hello

   readelf -d  (也可以看依賴的so文件)

   

常見的目標文件的格式有如下:

   

A.out 是最古老的目標文件格式。  現在用的最多的PE(Windows下用的最多的)、ELF(Linux下用的最多的)

目標文件我們一般也叫ABI(應用程式二進位介面)

實際上在目標文件中它已經是二進位相容(指目標文件已經是適應到某一個CPU體繫上的二進位指令)

比如x86上的可執行文件鏈接成ARM上的可執行文件肯定是不行的

ELF格式的文件中有三種主要的目標文件

  1. 可重定位(relocatable)文件保存著代碼和適當的數據,用來和其他的object文件一起來創建一個可執行文件或是一個共用文件(主要是 .o 文件)
  2. 一個可執行(executable)文件保存著一個用來執行的程式;該文件指出了exec(BA_OS)如何來創建序進程映像。
  3. 一個共用object文件保存這代碼和合適的數據,用來被下麵的兩個鏈接器鏈接。第一個連接編譯器(請查看ld(SD_CMD)),也可以和其他可重定位和共用object文件來創建他的object。第二是動態鏈接器,聯合一個可執行文件和其他的共用object文件來創建一個進程映像。(主要是 .so 文件)

靜態鏈接的ELF可執行文件於進程的地址空間:

對於32位的x86來講,進程是4G的進程地址空間,最上面1G是內核用的,下麵3G是用戶態。0xc000000之上是內核,下麵是用戶態可訪問。

預設進程是從0x8048000開始載入,先是頭部(大小不固定),還把代碼和數據載入到進程的地址空間。

ELF Header 中的Entry point address 即是可執行文件載入到記憶體中開始執行的第一行代碼。(就像上面的程式,他的值是0x8048320)

一般靜態鏈接會將所有代碼放在一個代碼段,而動態鏈接的進程會有多個代碼段。

 

二、編程使用exec*庫函數載入一個可執行文件

載入可執行程式前的工作:我們需要瞭解可執行程式的執行環境,一般是通過shell程式來啟動一個可執行程式。

當我們執行可執行文件時,即發起了一個系統調用exec時,我們就要知道shell環境位我們準備了哪些執行的上下文環境。

這樣我們就可以大概瞭解一下用戶態的執行環境,然後再看一個exec的系統調用,它怎麼把一個可執行文件載入在內核裡面,又返回到用戶它態。

命令行參數和shell 環境,一般我們執行一個程式的shell環境,我們的實驗直接使用execve系統調用。

$ls -l /usr/bin 列出/usr/bin下的目錄信息

shell本身不限制命令行參數的個數,命令行參數的個數受限於命令本身。

  例如, int main(int argc, char * argv[])

  又如, int main(int argc, char * argv[], char * envp[])

shell會調用execve將命令行參數和環境參數傳遞給可執行程式的main函數

  int execve(const char * filename, char * const argv[], char * const envp[])

  庫函數exec*都是execve的封裝常式

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 
  5 int main(int argc,char* argv[])
  6 {
  7     int pid;
  8     /*fork another process*/
  9     pid = fork();
 10     if(pid < 0)
 11     {
 12         /*error occurred*/
 13         fprintf(stderr,"Fork Failed!\n");
 14         exit(-1);
 15     }
 16     else if(pid == 0)
 17     {
 18         /* child process */
 19         execlp("/bin/ls","ls",NULL);
 20     }
 21     else
 22     {
 23         /* parent process */
 24         /* parent will wait for the child to complete */
 25         wait(NULL);
 26         printf("Child Complete\n");
 27         exit(0);
 28     }
 29 }
View Code

 

命令行參數和環境變數的保存和傳遞是當我們創建一個子進程時,(fork是複製父進程),然後調用exece系統調用,它把要載入的可執行程式把原來的進程環境給覆蓋掉了,覆蓋了以後它的用戶態堆棧也被清空。

這時命令行參數和環境變數會被壓棧。

shell程式 -> execve -> sys_execve   然後在初始化新進程堆棧時拷貝進去。

int execve(const char * filename , char * const argv[] , char * const envp[])

創建了一個新的用戶態堆棧的時候,實際上是把命令行參數(argv[])的內容和環境變數(envp[])的內容通過指針的方式傳遞給系統調用內核處理函數的,然後內核處理函數再建一個可執行程式新的用戶態堆棧的時候,會把這些拷貝到用戶態堆棧,初始化新的可執行程式執行的上下文環境。

先函數調用參數傳遞,再系統調用參數傳遞。

裝載時動態鏈接和運行時動態鏈接舉例:

 1 #include <stdio.h>
 2 
 3 #include "shlibexample.h" 
 4 
 5 #include <dlfcn.h>
 6 
 7 /*
 8  * Main program
 9  * input    : none
10  * output    : none
11  * return    : SUCCESS(0)/FAILURE(-1)
12  *
13  */
14 int main()
15 {
16     printf("This is a Main program!\n");
17     /* Use Shared Lib */
18     printf("Calling SharedLibApi() function of libshlibexample.so!\n");
19     SharedLibApi();
20     /* Use Dynamical Loading Lib */
21     void * handle = dlopen("libdllibexample.so",RTLD_NOW);
22     if(handle == NULL)
23     {
24         printf("Open Lib libdllibexample.so Error:%s\n",dlerror());
25         return   FAILURE;
26     }
27     int (*func)(void);
28     char * error;
29     func = dlsym(handle,"DynamicalLoadingLibApi");
30     if((error = dlerror()) != NULL)
31     {
32         printf("DynamicalLoadingLibApi not found:%s\n",error);
33         return   FAILURE;
34     }    
35     printf("Calling DynamicalLoadingLibApi() function of libdllibexample.so!\n");
36     func();  
37     dlclose(handle);       
38     return SUCCESS;
39 }
main.c
 1 #ifndef _DL_LIB_EXAMPLE_H_
 2 #define _DL_LIB_EXAMPLE_H_
 3 
 4 
 5 
 6 #ifdef __cplusplus
 7 extern "C" {
 8 #endif
 9 /*
10  * Dynamical Loading Lib API Example
11  * input    : none
12  * output    : none
13  * return    : SUCCESS(0)/FAILURE(-1)
14  *
15  */
16 int DynamicalLoadingLibApi();
17 
18 
19 #ifdef __cplusplus
20 }
21 #endif
22 #endif /* _DL_LIB_EXAMPLE_H_ */
dllibexample.h
 1 #include <stdio.h>
 2 #include "dllibexample.h"
 3 
 4 #define SUCCESS 0
 5 #define FAILURE (-1)
 6 
 7 /*
 8  * Dynamical Loading Lib API Example
 9  * input    : none
10  * output    : none
11  * return    : SUCCESS(0)/FAILURE(-1)
12  *
13  */
14 int DynamicalLoadingLibApi()
15 {
16     printf("This is a Dynamical Loading libary!\n");
17     return SUCCESS;
18 }
dllibexample.c
 1 #ifndef _SH_LIB_EXAMPLE_H_
 2 #define _SH_LIB_EXAMPLE_H_
 3 
 4 #define SUCCESS 0
 5 #define FAILURE (-1)
 6 
 7 #ifdef __cplusplus
 8 extern "C" {
 9 #endif
10 /*
11  * Shared Lib API Example
12  * input    : none
13  * output    : none
14  * return    : SUCCESS(0)/FAILURE(-1)
15  *
16  */
17 int SharedLibApi();
18 
19 
20 #ifdef __cplusplus
21 }
22 #endif
23 #endif /* _SH_LIB_EXAMPLE_H_ */
shlibexample.h
 1 #include <stdio.h>
 2 #include "shlibexample.h"
 3 
 4 /*
 5  * Shared Lib API Example
 6  * input    : none
 7  * output    : none
 8  * return    : SUCCESS(0)/FAILURE(-1)
 9  *
10  */
11 int SharedLibApi()
12 {
13     printf("This is a shared libary!\n");
14     return SUCCESS;
15 }
shlibexample.c

 編譯成libshlibexample.so

gcc -shared shlibexample.c -o libshlibexample.so -m32

編譯成libdllibexample.so

gcc -shared dllibexample.c -o libdllibexample.so -m32

分別以共用庫和動態載入共用庫的方式使用libshlibexample.so文件和libdllibexample.so文件

編譯main時,註意這裡只提供shlibexample的-L(庫對應的介面頭文件所在的目錄),和-l(庫名,如libshlibexample.so去掉lib和.so的部分)

並沒有提供dllibexample的相關信息,只是指名了-dll

gcc main.c -o main -L ~/my/c/SharedLibDynamicLink -lshlibexample -ldl -m32         (其中 ~/my/c/SharedLibDynamicLink 是我的這幾個文件所在的路徑)

export LD_LIBRARY_PATH=$PWD    (將當前目錄加入預設路徑,否則main找不到依賴的庫文件,當然也可以以將庫文件copy到預設路徑下)

./main

運行結果如下:

 

三、可執行程式的裝載

sys_execve內部會解析可執行文件格式。在 /linux-3.18.6/fs/exec.c

1604SYSCALL_DEFINE3(execve,
1605        const char __user *, filename,
1606        const char __user *const __user *, argv,
1607        const char __user *const __user *, envp)
1608{
1609    return do_execve(getname(filename), argv, envp);
1610}
View Code
1549int do_execve(struct filename *filename,
1550    const char __user *const __user *__argv,
1551    const char __user *const __user *__envp)
1552{
1553    struct user_arg_ptr argv = { .ptr.native = __argv };
1554    struct user_arg_ptr envp = { .ptr.native = __envp };
1555    return do_execve_common(filename, argv, envp);
1556}
do_execve

do_execve -> do_execve_common -> exec_binprm

1549int do_execve(struct filename *filename,
1550    const char __user *const __user *__argv,
1551    const char __user *const __user *__envp)
1552{
1553    struct user_arg_ptr argv = { .ptr.native = __argv };
1554    struct user_arg_ptr envp = { .ptr.native = __envp };
1555    return do_execve_common(filename, argv, envp);
1556}
do_execve
1427/*
1428 * sys_execve() executes a new program.
1429 */
1430static int do_execve_common(struct filename *filename,
1431                struct user_arg_ptr argv,
1432                struct user_arg_ptr envp)
1433{
1434    struct linux_binprm *bprm;
1435    struct file *file;
1436    struct files_struct *displaced;
1437    int retval;
1438
1439    if (IS_ERR(filename))
1440        return PTR_ERR(filename);
1441
1442    /*
1443     * We move the actual failure in case of RLIMIT_NPROC excess from
1444     * set*uid() to execve() because too many poorly written programs
1445     * don't check setuid() return code.  Here we additionally recheck
1446     * whether NPROC limit is still exceeded.
1447     */
1448    if ((current->flags & PF_NPROC_EXCEEDED) &&
1449        atomic_read(&current_user()->processes) > rlimit(RLIMIT_NPROC)) {
1450        retval = -EAGAIN;
1451        goto out_ret;
1452    }
1453
1454    /* We're below the limit (still or again), so we don't want to make
1455     * further execve() calls fail. */
1456    current->flags &= ~PF_NPROC_EXCEEDED;
1457
1458    retval = unshare_files(&displaced);
1459    if (retval)
1460        goto out_ret;
1461
1462    retval = -ENOMEM;
1463    bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
1464    if (!bprm)
1465        goto out_files;
1466
1467    retval = prepare_bprm_creds(bprm);
1468    if (retval)
1469        goto out_free;
1470
1471    check_unsafe_exec(bprm);
1472    current->in_execve = 1;
1473
1474    file = do_open_exec(filename);
1475    retval = PTR_ERR(file);
1476    if (IS_ERR(file))
1477        goto out_unmark;
1478
1479    sched_exec();
1480
1481    bprm->file = file;
1482    bprm->filename = bprm->interp = filename->name;
1483
1484    retval = bprm_mm_init(bprm);
1485    if (retval)
1486        goto out_unmark;
1487
1488    bprm->argc = count(argv, MAX_ARG_STRINGS);
1489    if ((retval = bprm->argc) < 0)
1490        goto out;
1491
1492    bprm->envc = count(envp, MAX_ARG_STRINGS);
1493    if ((retval = bprm->envc) < 0)
1494        goto out;
1495
1496    retval = prepare_binprm(bprm);
1497    if (retval < 0)
1498        goto out;
1499
1500    retval = copy_strings_kernel(1, &bprm->filename, bprm);
1501    if (retval < 0)
1502        goto out;
1503
1504    bprm->exec = bprm->p;
1505    retval = copy_strings(bprm->envc, envp, bprm);
1506    if (retval < 0)
1507        goto out;
1508
1509    retval = copy_strings(bprm->argc, argv, bprm);
1510    if (retval < 0)
1511        goto out;
1512
1513    retval = exec_binprm(bprm);
1514    if (retval < 0)
1515        goto out;
1516
1517    /* execve succeeded */
1518    current->fs->in_exec = 0;
1519    current->in_execve = 0;
1520    acct_update_integrals(current);
1521    task_numa_free(current);
1522    free_bprm(bprm);
1523    putname(filename);
1524    if (displaced)
1525        put_files_struct(displaced);
1526    return retval;
1527
1528out:
1529    if (bprm->mm) {
1530        acct_arg_size(bprm, 0);
1531        mmput(bprm->mm);
1532    }
1533
1534out_unmark:
1535    current->fs->in_exec = 0;
1536    current->in_execve = 0;
1537
1538out_free:
1539    free_bprm(bprm);
1540
1541out_files:
1542    if (displaced)
1543        reset_files_struct(displaced);
1544out_ret:
1545    putname(filename);
1546    return retval;
1547}
do_execve_common
1405static int exec_binprm(struct linux_binprm *bprm)
1406{
1407    pid_t old_pid, old_vpid;
1408    int ret;
1409
1410    /* Need to fetch pid before load_binary changes it */
1411    old_pid = current->pid;
1412    rcu_read_lock();
1413    old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));
1414    rcu_read_unlock();
1415
1416    ret = search_binary_handler(bprm);
1417    if (ret >= 
              
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1. Objc是一門編譯型語言,JAVA是解析型語言 編譯型語言:把做好的源程式全部編譯成二進位代碼的可運行程式。然後,可直接運行這個程式。 編譯型語言,執行速度快、效率高;依賴編譯器、跨平臺性差些。 解析型語言:解釋性語言在運行程式的時候才翻譯,每個語句都是執行的時候才翻譯。這樣解釋性語言每執行一 ...
  • 在我學習hive的時候,按照官網上的demo, // sc is an existing SparkContext. val sqlContext = new org.apache.spark.sql.hive.HiveContext(sc) sqlContext.sql("CREATE TABLE ...
  • 介紹 本篇文章主要從查看MySQL的啟動命令的代碼來詳細瞭解MySQL的啟動過程,內容多為概念知識;理解MySQL的啟動原理對熟悉MySQL至關重要,啟動mysql服務有三種方式分別是:mysql.sever,mysqld,mysqld_safe。 my.cnf mysql.server 預設的my ...
  • Knowledge Dependence:閱讀文本前,你需要瞭解基本的關係型資料庫與非關係型(NoSQL)資料庫的概念和區別,以及 MongoDB(Mongoose)的簡單實踐。 ​ 這兩三年來,伴隨著大數據(Big Data)的空前火熱,無論是在工程界還是科研界,非關係型資料庫(NoSQL)都已經 ...
  • 實驗環境:Ubnuntu 64位(推薦使用14.04)+Xshell 阿裡雲現在提供的雲伺服器很好用的,用來編譯內核性能也不錯。本文介紹最基本的內核編譯方法,為了方便,所有操作均在root用戶下進行。 如果不是root用戶可以使用su命令切換到root用戶。 註:使用xshell的時候最好把這一項給 ...
  • In order to provide an environment for network experiments in the future, I use VirutalBox to create some hosts. My real host is Ubuntu 14.04 LTS. And ...
  • 1.安裝MySQL需要的依賴的包和編譯軟體 (1)安裝MySQL需要的依賴包 安裝MySQL之前,最好先安裝MySQL需要的依賴包,不然後面會出現報錯,還得回來安裝MySQL的依賴包。 [root@Mysql-server tools]# yum install -y ncurses-devel l ...
  • 首先從www.zabbix.com下載rpm包: 接下來我要配置一臺zabbix server,自己監控自己即使服務端又是客戶端,zabbix web gui和zabbix資料庫都放在同一臺主機上,除了第4,5個包不裝外,其他的包都給裝上,為瞭解決某些包的依賴關係還需事先安裝yum -y insta ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...