zoukankan      html  css  js  c++  java
  • Linux Ptrace 详解

    转 https://blog.csdn.net/u012417380/article/details/60470075

    Linux Ptrace 详解

    一、系统调用

    操作系统提供一系列系统调用函数来为应用程序提供服务。关于系统调用的详细相关知识,可以查看<<程序员的自我修养》第十二章。
    对于x86操作系统来说,用中断命令“int 0x80”来进行系统调用,系统调用前,需要将系统调用号放入到%EAX寄存器中,将系统的参数依次放入到寄存器%ebx%ecx%edx以及%esi%edi中。

    以write系统调用为例:

    write(2,"Hello",5);
    • 1

    在32位系统中会转换成:

    movl $1,%eax
    movl $2,%ebx
    movl $hello,%ecx
    movl $5,%edx
    int $0x80
    • 1
    • 2
    • 3
    • 4
    • 5

    其中1为write的系统调用号,所有的系统调用号定义在unistd.h文件中,$hello 表示字符串“Hello”的地址;32位Linux系统通过0x80中断来进行系统调用。

    64位系统用户应用层用整数寄存器%rdi ,%rsi,%rdx,%rcx, %r8以及 %r9来传参。而内核接口用%rdi ,%rsi,%rdx,%r10,&r8以及%r10来传参,并且用syscall指令而不是80中断进行系统调用。
    x86和x64都用寄存器rax来保存调用号和返回值。

    二、ptrace 函数简介

    #include <sys/ptrace.h>
    
    long ptrace(enum _ptrace_request request,pid_t pid,void * addr ,void *data);
    • 1
    • 2
    • 3

    ptrace()系统调用函数提供了一个进程(the “tracer”)监察和控制另一个进程(the “tracee”)的方法。并且可以检查和改变“tracee”进程的内存和寄存器里的数据。它可以用来实现断点调试和系统调用跟踪。

    tracee首先需要被附着到tracer。在多线程进程中,每个线程都可以被附着到一个tracer。ptrace命令总是以ptrace(PTARCE_foo,pid,..)的形式发送到tracee进程。pid是tracee线程ID。

    当一个进程可以开始跟踪进程通过调用fork函数创建子进程并让子进程执行PTRACE_TRACEME,然后子进程再调用execve()(如果当前进程被ptrace,execve()成功执行后 SIGTRAP信号量会被发送到该进程)。一个进程也可以使用”PTRACE_ATTACH”或者”PTRACE_SEIZE”来跟踪另一个进程。

    当进程被跟踪后,每当信号量传来,甚至信号量会被忽略时,tracee会暂停。tracer会在下次调用waitpid(wstatus)(或者其它wait系统调用)处被通知。该调用会返回一个包含tracee暂停原因信息的状态码。当tracee暂停后,tracer可以使用一系列ptrace请求来查看和修改tracee中的信息。tracer接着可以让tracee继续执行。tracee传递给tracer中的信号量通常被忽略。
    当PTRACE_O_TRACEEXEC项未起作用时,所有成功执行execve()的tracee进程会被发送一个 SIGTRAP信号量后暂停,在新程序执行之前,父进程将会取得该进程的控制权。

    当tracer结束跟踪后,可以通过调用PTRACE_DETACH继续让tracee执行。

    prace更多相关信息可以查看http://man7.org/linux/man-pages/man2/ptrace.2.html官方文档。

    三、示例

    1.ptrace追踪子进程执行exec()

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/ptrace.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <sys/reg.h>   /* For constants ORIG_RAX etc */
    int main(){
       pid_t child;
       long orig_rax;
       child=fork();
       if(child==0){
          ptrace(PTRACE_TRACEME,0,NULL,NULL);
          execl("/bin/ls","ls",NULL);
       }else{
            wait(NULL);
            orig_rax = ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);
            printf("The child made a system call %ld
    ",orig_rax);
            ptrace(PTRACE_CONT,child,NULL,NULL);
       }
    
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    编译后输出:

    The child made a system call 59
    user1@user-virtual-machine:~/hookTest$ a.out    attach.c~  ex1.c   ex1.o  ex2.c~  ex3.c   ex3.o  ex4.c~    victim.c~
    attach.c  attach.o   ex1.c~  ex2.c  ex2.o   ex3.c~  ex4.c  victim.c  victim.o
    
    • 1
    • 2
    • 3
    • 4

    execl()函数对应的系统调用为__NR_execve,系统调用值为59。父进程通过调用fork()来创建子进程。在子进程中,先运行patrce().请求参数设为PTRACE_TRACE,来告诉内核当前进程被父进程trace,每当有信号量传递到当前进程,该进程会暂停,提醒父进程在wait()调用处继续执行。然后再调用execl()。当execl()函数成功执行后,新程序运行之前,SIGTRAP信号量会被发送到该进程,让子进程停止,这时父进程会在wait相关调用处被通知,获取子进程的控制权,可以查看子进程内存和寄存器相关信息。

    当进程进行系统调用时,int会在内核栈中依次压入用户态的寄存器SS、ESP、EFLAGS、CS、EIP.中断处理程序的SAVE_ALL宏会将 依次将EAX、EBP、EDI、ESI、EDX、ECX、EBX寄存器值压入内核栈。调用ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL) 获取USER area信息时<sys/reg.h>文件定义了与内核栈寄存器数组顺序相同的下标:

    
    #ifndef _SYS_REG_H
    #define _SYS_REG_H  1
    
    
    #ifdef __x86_64__
    /* Index into an array of 8 byte longs returned from ptrace for
       location of the users' stored general purpose registers.  */
    
    # define R15    0
    # define R14    1
    # define R13    2
    # define R12    3
    # define RBP    4
    # define RBX    5
    # define R11    6
    # define R10    7
    # define R9 8
    # define R8 9
    # define RAX    10
    # define RCX    11
    # define RDX    12
    # define RSI    13
    # define RDI    14
    # define ORIG_RAX 15
    # define RIP    16
    # define CS 17
    # define EFLAGS 18
    # define RSP    19
    # define SS 20
    # define FS_BASE 21
    # define GS_BASE 22
    # define DS 23
    # define ES 24
    # define FS 25
    # define GS 26
    #else
    
    /* Index into an array of 4 byte integers returned from ptrace for
     * location of the users' stored general purpose registers. */
    
    # define EBX 0
    # define ECX 1
    # define EDX 2
    # define ESI 3
    # define EDI 4
    # define EBP 5
    # define EAX 6
    # define DS 7
    # define ES 8
    # define FS 9
    # define GS 10
    # define ORIG_EAX 11
    # define EIP 12
    # define CS  13
    # define EFL 14
    # define UESP 15
    # define SS   16
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    这样8*ORIG_RAX就找到USER area 中 ORIG_RAX 寄存器值的保存地址。ORIG_RAX保存了系统调用号。

    当检查完系统调用之后,可以调用ptrace并设置参数PTRACE_CONT让子进程继续进行。

    2.读取子进程系统调用参数

    //64位下乌班图程序
    
    #include <sys/ptrace.h>
    #include <sys/wait.h>
    #include <sys/reg.h>
    #include <sys/user.h>
    #include <sys/syscall.h>
    #include <stdio.h>
    int main(){
        pid_t child;
        long orig_rax;
        int status;
        int iscalling=0;
        struct user_regs_struct regs;
    
        child = fork();
            if(child==0){
              ptrace(PTRACE_TRACEME,0,NULL,NULL);
          execl("/bin/ls","ls","-l","-h",NULL);
        }else{
              while(1){
            wait(&status);
                    if(WIFEXITED(status))
                        break;
                    orig_rax=ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);
                    if(orig_rax == SYS_write){
                        ptrace(PTRACE_GETREGS,child,NULL,&regs);
                if(!iscalling){
                    iscalling =1;
                    printf("SYS_write call with %lld, %lld, %lld
    ",regs.rdi,regs.rsi,regs.rdx);
                            } else{
                                   printf("SYS_write call return %lld
    ",regs.rax);
                                   iscalling = 0;
                            }                                  
    
    
                    }
                ptrace(PTRACE_SYSCALL,child,NULL,NULL);
              }
           }
              return 0;
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    编译后输出:

    SYS_write call with 1, 140179049189376, 14
    总用量 28K
    SYS_write call return 14
    SYS_write call with 1, 140179049189376, 51
    -rw-rw-r-- 1 user1 user1  534  2月 26 18:02 ex1.c
    SYS_write call return 51
    SYS_write call with 1, 140179049189376, 52
    -rw-rw-r-- 1 user1 user1  534  2月 26 18:02 ex1.c~
    SYS_write call return 52
    SYS_write call with 1, 140179049189376, 53
    -rw-rw-r-- 1 user1 user1 1.1K  3月  2 13:02 hook2.c
    SYS_write call return 53
    SYS_write call with 1, 140179049189376, 54
    -rw-rw-r-- 1 user1 user1 1.1K  3月  2 13:02 hook2.c~
    SYS_write call return 54
    SYS_write call with 1, 140179049189376, 53
    -rwxrwxr-x 1 user1 user1 8.6K  3月  2 13:02 hook2.o
    SYS_write call return 53
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    可以看到ls -l -h 执行了六次SYS_write系统调用。
    读取寄存器中的参数时,可以使用PTRACE_PEEKUSER一个字一个字读取,也可以使用PTRACE_GETREGS参数直接将寄存器的值读取到结构体user_regs_struct 中,该结构体定义在sys/user.h

    对于PTRACE_STSCALL参数,该参数会像PTRACE_CONT一样使暂停的子进程继续执行,并在子进程下次进行系统调用前或系统调后,向子进程发送SINTRAP信号量,让子进程暂停。

    WIFEXITED函数(宏)函数用来检查子进程是暂停还准备退出。

    3.修改子进程系统调用参数

    val = ptrace(PTRACE_PEEKDATA,child,addr,NULL)
    • 1

    PTRACE_PEEKDATAPTRACE_PEEKTEXT参数是在tracee内存的addr地址处读取一个字(sizeof(long))的数据,反回值是long 型的,可多次读取addr
    +i*sizeof(long)然后再合并得到最终字符串的内容。

    现在,我们对系统调用write 输出的字符串参数进行反转:

    #include <sys/ptrace.h>
    #include <sys/wait.h>
    #include <sys/reg.h>
    #include <sys/syscall.h>
    #include <sys/user.h>
    #include <stdio.h>
    #include <string.h>
    #include <errno.h>
    #include <stdlib.h>
    #define long_size sizeof(long)
    
    void reverse(char * str)
    {
        int i,j;
        char temp;
        for(i=0,j=strlen(str)-2;i<=j;++i,--j){
              temp=str[i];
              str[i]=str[j];
              str[j]=temp;
        }
    }
    
    
    void getdata(pid_t child,long addr,char * str,int len){
      char * laddr;
      int i,j;
      union u{
        long val;
            char chars[long_size];
      } data;
      i=0;
      j=len/long_size;
      laddr=str;
      while(i<j){
       data.val=ptrace(PTRACE_PEEKDATA,child,addr+i*long_size,NULL);
       if(data.val == -1){
         if(errno){
            printf("READ error: %s
    ",strerror(errno));
         }
       }
         memcpy(laddr,data.chars,long_size);
         ++i;
         laddr +=long_size;
      };
      j=len % long_size;
      if(j!=0){
         data.val=ptrace(PTRACE_PEEKDATA,child,addr+i*long_size,NULL);
         memcpy(laddr,data.chars,j);
      }
       str[len]='';
    }
    
    void putdata(pid_t child,long addr,char * str,int len){
        char * laddr;
            int i,j;
            union u{
              long val;
              char chars[long_size];
           } data;
           i=0;
           j=len /long_size;
           laddr=str;
           while(i<j){
               memcpy(data.chars,laddr,long_size);
               ptrace(PTRACE_POKEDATA,child,addr +i*long_size,data.val);
               ++i;
               laddr+=long_size;
         }
         j=len%long_size;
         if(j!=0){   
               //注意:由于写入时也是按字写入的,所以正确的做法是先将该字的高地址数据读出保存在data的高地址上 ,然后将该字再写入
               memcpy(data.chars,laddr,j);
               ptrace(PTRACE_POKEDATA,child,addr +i*long_size,data.val);
         }
    
    }
    
    int main(){
        pid_t child;
            int status;
            struct user_regs_struct regs;
            child =fork();
            if(child ==0){
              ptrace(PTRACE_TRACEME,0,NULL,NULL);
              execl("/bin/ls","ls",NULL);
           }else{
               long orig_eax;
    
               char *str,*laddr;
               int toggle =0;
               while(1){
                 wait(&status);
                 if(WIFEXITED(status))
                     break;
                 orig_eax = ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);
                 if(orig_eax == SYS_write){
                   if(toggle == 0){
                      toggle =1;
                     ptrace(PTRACE_GETREGS,child,NULL,&regs);
    
                     str=(char * )calloc((regs.rdx+1),sizeof(char));
                     getdata(child,regs.rsi,str,regs.rdx);
                     reverse(str);
                     putdata(child,regs.rsi,str,regs.rdx);
                  }else{
                  toggle =0;
                   }
                }
    
             ptrace(PTRACE_SYSCALL,child,NULL,NULL);
              }
           }
          return 0;
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117

    输出:

    user1@user-virtual-machine:~/hookTest$ ./hook3.o
    o.3kooh  ~c.3kooh  c.3kooh  o.2kooh  ~c.2kooh c.2kooh  ~c.1xe  c.1xe
    
    • 1
    • 2
    • 3

    4.向其它程序注入指令

    我们追踪其它独立运行的进程时,需要使用下面的命令:

    ptrace(PTRACE_ATTACH, pid, NULL, NULL)
    • 1

    使pid进程成为被追踪的tracee进程。tracee进程会被发送一个SIGTOP信号量,tracee进程不会立即停止,直到完成本次系统调用。如果要结束追踪,则调用PTRACE_DETACH即可。

    debug 设置断点的功能可以通过ptrace实现。原理是ATTACH正在运行的进程使其停止。然后读取该进程的指令寄存器IR(32位x86为EIP,64w的是RIP)内容所指向的指令,备份后替换成目标指令,再使其继续执行,此时被追踪进程就会执行我们替换的指令,运行完注入的指令之后,我们再恢复原进程的IR
    ,从而达到改变原程序运行逻辑的目的。

    tracee进程代码:

    stdio.h>
    
    
    int main(){
            int i=0;
        while(1){
                printf("Hello,ptrace! [pid:%d]! num is %d
    ",getpid(),i++);
                    sleep(2);
          }
          return 0;
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    tracer进程代码

    
    #include<sys/ptrace.h>
    #include<sys/reg.h>
    #include<sys/wait.h>
    #include<sys/user.h>
    #include<stdlib.h>
    #include<errno.h>
    #include<string.h>
    #include<stdio.h>
    
    #define long_size sizeof(long)
    
    
    void getdata(pid_t child, long addr ,char * str,int len){
        char * laddr =str;
        int i,j;
        union u{
              long  val;
              char   chars [long_size] ;
            } data;
        i=0;
        j=len/long_size;
    
         while(i<j){
               data.val=ptrace(PTRACE_PEEKDATA,child,addr + long_size*i,NULL);
               if(data.val==-1){
            if(errno){
                      printf("READ error: %s
    ",strerror(errno));
                    }
               }
               memcpy(laddr,data.chars,long_size);
                ++i; 
               laddr=laddr+long_size;
            }
    
        j= len %long_size;
        if(j!=0){
          data.val=ptrace(PTRACE_PEEKDATA,child,addr + long_size*i,NULL);
          if(data.val==-1){
            if(errno){
                      printf("READ error: %s
    ",strerror(errno));
                    }
               }
          memcpy(laddr,data.chars,j);
        }
        str[len]='';
    }
    
    void putdata(pid_t child , long addr,char * str,int len){
        char * laddr =str;
        int i,j;
        j=len/long_size;
        i=0;
        union u{
               long val;
               char chars [long_size]  ;
        } data;
        while(i<j){
             memcpy(data.chars,laddr,long_size);
             ptrace(PTRACE_POKEDATA,child,addr + long_size*i,data.val);
             ++i;
             laddr=laddr+long_size;
        }
        j=len%long_size;
        if(j!=0){
            data.val= ptrace(PTRACE_PEEKDATA,child,addr + long_size*i,NULL);
            if(data.val==-1){
               if(errno){
                      printf("READ error: %s
    ",strerror(errno));
                    }
            }
    
             memcpy(data.chars,laddr,j);
             ptrace(PTRACE_POKEDATA,child,addr + long_size*i,data.val);   
         }     
    }
    
    int main(int argc,char * argv[]){
         if(argc!=2){
             printf("Usage: %s pid
    ",argv[0]);
         }
         pid_t tracee = atoi(argv[1]);
         struct user_regs_struct regs;
         /*int 80(系统调用) int 3(断点)*/
         unsigned char code[]={0xcd,0x80,0xcc,0x00,0,0,0,0}; //八个字节,等于long 型的长度
         char backup[8]; //备份读取的指令
         ptrace(PTRACE_ATTACH,tracee,NULL,NULL);
         long inst;  //用于保存指令寄存器所指向的下一条将要执行的指令的内存地址 
    
          wait(NULL);
          ptrace(PTRACE_GETREGS,tracee,NULL,&regs);
         inst  =ptrace(PTRACE_PEEKTEXT,tracee,regs.rip,NULL);
          printf("tracee:RIP:0x%llx INST: 0x%lx
    ",regs.rip,inst);
         //读取子进程将要执行的 7 bytes指令并备份
         getdata(tracee,regs.rip,backup,7);
         //设置断点
         putdata(tracee,regs.rip,code,7);
         //让子进程继续执行并执行“int 3”断点指令停止
         ptrace(PTRACE_CONT,tracee,NULL,NULL);
    
         wait(NULL);
         long rip=ptrace(PTRACE_PEEKUSER,tracee,8*RIP,NULL);//获取子进程停止时,rip的值
         long inst2=ptrace(PTRACE_PEEKTEXT,tracee,rip,NULL);
         printf("tracee:RIP:0x%lx INST: 0x%lx
    ",rip,inst2);
    
    
         printf("Press Enter to continue  tracee process
    ");
         getchar();
         putdata(tracee,regs.rip,backup,7); //重新将备份的指令写回寄存器
         ptrace(PTRACE_SETREGS,tracee,NULL,&regs);//设置会原来的寄存器值
          ptrace(PTRACE_CONT,tracee,NULL,NULL);
         ptrace(PTRACE_DETACH,tracee,NULL,NULL);
         return 0;
    
    
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116

    先运行tracee.o 文件

    $  ./tracee.o
    • 1

    此时tracee.o输出:

    Hello,ptrace! [pid:14384]! num is 0
    Hello,ptrace! [pid:14384]! num is 1
    Hello,ptrace! [pid:14384]! num is 2
    Hello,ptrace! [pid:14384]! num is 3
    ......
    • 1
    • 2
    • 3
    • 4
    • 5

    再另打开一个shell运行attach.o文件

    $  ./.attach.o  14384 //pid
    • 1

    此时tracee.o执行到int 3断点指令停止,attach1,o输出:

    tracee:RIP:0x7f48b0394f20 INST: 0x3173fffff0013d48
    tracee:RIP:0x7f48b0394f23 INST: 0x8348c33100000000
    Press Enter to continue  tracee process
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    按任意键tracee.o恢复执行

    参考:

    http://www.cnblogs.com/pannengzhi/p/5203467.html

  • 相关阅读:
    python3 访问 rabbitmq 示例
    centos7 GNOME 安装微信客户端
    使用 rm -rf 删除了工程目录,然后从 pycharm 中找了回来
    主动做事,做一个靠谱的人
    Go net/http 发送常见的 http 请求
    学会感激,才能长大
    Go context 介绍和使用
    xargs 命令
    Docker 镜像 && 容器的基本操作
    CentOS && Ubuntu 环境下 Docker 的安装配置
  • 原文地址:https://www.cnblogs.com/yibutian/p/9482972.html
Copyright © 2011-2022 走看看