ebpf原理分析

 ebpf起源於bpf(Berkeley Packet Filter),bpf是一種網絡過濾框架,爲了向後兼容,現在也稱爲cbpf。
 bpf和ebpf主要有以下不同。
 bpf僅限於網絡性能監控,ebpf已經擴展到內核追蹤、性能監控和traffice control多個領域。向下,已經涵蓋kprobe、tracepoinut、uprobe、profile和watchpoint等調試接口,向上又在接口設計和易用性上做了較大改進,目前主流使用工具爲bcc和bpftrace。
 同時,ebpf指令和寄存器的更接近於64位處理器,內核JIT編譯的效率更高。數據通信方面,ebpf拋棄了bpf的socket通信機制,採用了map機制,更加豐富高效。
 ebpf屬於一種駐留在內核的虛擬機,本質是代碼注入技術,通過注入控制邏輯實現用戶的監控和調試目的,map機制用來實現用戶和內核的數據交換和管理。本文主要通過簡單bpftrace和bcc例子分析ebpf的prog注入流程和map機制。
 prog注入流程:

  • c代碼控制邏輯通過llvm和clang編譯成ebpf彙編程序
  • 通過bpf系統調用加載ebpf的prog到內核,對ebpf程序進行verfify,通過之後再JIT在線編譯成本機可執行指令
  • 將JIT編譯出的可執行程序關聯到kprobe、tracepoint的hook上
     看一個內核中標示prog的結構體:
 struct bpf_prog {
     u16         pages;      /* 分配page數 */
     u16         jited:1,    /* prog是否已經jit過*/
                 jit_requested:1,/* 是否需要jit */
                 undo_set_mem:1, /* Passed set_memory_ro() checkpoint */
                 gpl_compatible:1, /* Is filter GPL compatible? */
                 cb_access:1,    /* Is control block accessed? */
                 dst_needed:1,   /* Do we need dst entry? */
                 blinded:1,  /* 常量致盲 */
                 is_func:1,  /* ebpf func? 大多數情況是 */
                 kprobe_override:1, /* 是否是overrided kprobe */
                 has_callchain_buf:1; /* callchain buffer allocated? */
     enum bpf_prog_type  type;       /* prog類型,eg kprobe 、tracepoint*/
     enum bpf_attach_type    expected_attach_type; /* For some prog types */
     u32         len;        /* ebpf指令個數 */
     u32         jited_len;  /* ebpf彙編指令代碼總長度 */
     u8          tag[BPF_TAG_SIZE];
     struct bpf_prog_aux *aux;       /* Auxiliary fields */
     struct sock_fprog_kern  *orig_prog; /* Original BPF program */
     unsigned int        (*bpf_func)(const void *ctx,
                         const struct bpf_insn *insn);/* 存放jit後的可執行彙編 */
     /* 不支持jit,需要模擬,x64支持jit,不需要模擬 */
     union { 
         struct sock_filter  insns[0]; /* 從用戶態拷貝來的ebpf原程序 */
         struct bpf_insn     insnsi[0];
     };
 };

 struct bpf_prog作爲ebpf注入程序的載體,比較重要的是bpf_func成員,這個是ebpf程序jit後的本機可執行程序,是用戶控制邏輯在內核中的體現,jited_len是其長度。insns存放從用戶拷貝來的原ebpf彙編程序。
 在看ebpf prog load流程之前先看下ebpf寄存器和x64的對應關係
 ebpf從bpf的兩個32位寄存器擴展到10個64位寄存器R0~R9和一個只讀棧幀寄存器,並支持call指令,更加貼近現代64位處理器硬件

  • R0對應rax, 函數返回值
  • R1對應rdi, 函數參數1
  • R2對應rsi, 函數參數2
  • R3對應rdx, 函數參數3
  • R4對應rcx, 函數參數4
  • R5對應r8, 函數參數5
  • R6對應rbx, callee保存
  • R7對應r13, callee保存
  • R8對應r14, callee保存
  • R9對應r15, callee保存
  • R10對應rbp,只讀棧幀寄存器
    可以看到x64的r9寄存器沒有ebpf寄存器對應,所以ebpf函數最多支持5個參數。

 看下ebpf prog的load流程:
 bpf系統調用調用bpf_prog_load來加載ebpf程序。
 bpf_prog_load大致有以下幾步:

  • 1.調用bpf_prog_alloc爲prog申請內存,大小爲struct bpf_prog大小+ebpf指令總長度
  • 2.將ebpf指令複製到prog->insns
  • 3.調用bpf_check對ebpf程序合法性進行檢查,這是ebpf的安全性的關鍵所在,不符ebpf規則的load失敗
  • 4.調用bpf_prog_select_runtime在線jit,編譯ebpf指令成x64指令
  • 5.調用bpf_prog_alloc_id爲prog生成id,作爲prog的唯一標識的id被很多工具如bpftool用來查找prog

 其中第4步的bpf_check函數會對ebpf程序進行各種檢查,確保安全性,主要包括:

  • 1.調用replace_map_fd_with_map_ptr將ebpf彙編中的fd替換爲對應的map結構體地址。在需要map通信的之後,llvm和clang會使用map的fd來標識map機構體地址,並把fd作爲參數給map helper輸出函數使用,所以這裏需要進行轉換。這裏需要構造個load 64-bit immediate nsn,就是兩個指令,原指令和後面一個空白指令,把map結構體的地址分成前後32位分別存入這兩條指令的imm域。可見在編譯成ebpf指令的時候,load map fd的指令下一個指令是無效指令,供填充
  • 2.check_subprogs檢查所有條件跳轉指令都位於相應subprog內(本ebpf函數內)。首先遍歷prog中所有的指令,根據函數首地址,生成subprog(一個bebpf可能有多個函數),並對subprog以函數首地址以順序的方式插入到subprog_info數組裏去。最後遍歷所有指令,確保所有的條件跳轉指令必須位於本函數體地址之內。第一個subprog是bpf的main函數。bpf指令支持兩種call調用,一種bpf函數對bpf函數的調用,一種是bpf中對內核helper func的調用。前者是指令class爲BPF_JMP,指令opcode位BPF_CALL,並且src寄存器爲BPF_PSEUDO_CALL,指令imm爲callee函數到本指令的距離。另外一種是對對內核helper func的調用,指令class爲特徵是BPF_JMP,指令opcode爲BPF_CALL,並且src_reg=0,指令imm在jit之前位內核helper func id,jit之後爲對應的func到本指令的距離。
  • 3.check_cfg採用深度優先算法確保函數分支不存在循環和存在執行不到的指令。
  • 4.do_check函數檢查寄存器和參數的合法性。在檢查的過程中,以函數爲維度會記錄ebpf的10個寄存器和每個棧數據的訪問權限狀態,對於沒有被讀寫過的寄存器,訪問不被允許。寫R10寄存器是非法的,R10寄存器對應x64的bp寄存器,但ebpf的是隻讀的。每個指令經過檢查之後,更新寄存器和棧內存的權限狀態。
  • 5.check_max_stack_depth函數確保函數調用深度不超過8(MAX_CALL_FRAMES)。該函數遍歷ebpf所有指令,並在遇到opcode是BPF_CALL指令並且src_reg是BPF_PSEUDO_CALL的指令(也就是ebpf函數對ebpf函數的調用)時候進行函數模擬調用,並記錄調用深度和返回地址,函數執行完,返回上一層subprog,在這期間如果調用深度超過8或者最大棧消耗超過512字節,返回失敗。
  • 6.fixup_bpf_calls函數用於修正BPF_CALL指令(helper func調用)。
  • 7.調用fix_call_args函數對多bpf函數的prog進行jit。這裏注意,如果prog是包含多個ebpf函數,調用jit_subprogs函數進行jit。如果是單bpf函數的prog,函數返回。單bpf函數的prog的jit放到bpf_prog_select_runtime函數進行。

 先看下第2步的check_subprogs函數:

static int check_subprogs(struct bpf_verifier_env *env)
{
    int i, ret, subprog_start, subprog_end, off, cur_subprog = 0;
    struct bpf_subprog_info *subprog = env->subprog_info;
    struct bpf_insn *insn = env->prog->insnsi;
    int insn_cnt = env->prog->len;

    /* 添加main函數到env->subprog_info數組 */
    ret = add_subprog(env, 0);
    if (ret < 0)
        return ret;

    /* 遍歷所有prog指令,只關注bpf到bpf函數調用,把所有的bpf函數以函數首地址爲key升序加入env->subprog_info數組*/
    for (i = 0; i < insn_cnt; i++) {
        if (insn[i].code != (BPF_JMP | BPF_CALL))
            continue;
        if (insn[i].src_reg != BPF_PSEUDO_CALL)
            continue;
        if (!env->allow_ptr_leaks) {
            verbose(env, "function calls to other bpf functions are allowed for root only\n");
            return -EPERM;
        }
        /* 走到這裏的都是bpf到bpf函數調用的call指令,i+insn[i].imm+1爲調用子函數的首地址 */
        ret = add_subprog(env, i + insn[i].imm + 1);
        if (ret < 0)
            return ret;
    }

    subprog[env->subprog_cnt].start = insn_cnt;

    if (env->log.level > 1)
        for (i = 0; i < env->subprog_cnt; i++)
            verbose(env, "func#%d @%d\n", i, subprog[i].start);

    /* 遍歷prog所有指令,這次只關注跳轉指令*/
    /* 由於env->subprog_info數組裏是以函數首地址升序排序,
    那麼,suprog_info[i].start和subprog_info[i+1].start就是第i個函數的其實地址*/
    subprog_start = subprog[cur_subprog].start;
    subprog_end = subprog[cur_subprog + 1].start;
    for (i = 0; i < insn_cnt; i++) {
        u8 code = insn[i].code;

        if (BPF_CLASS(code) != BPF_JMP)
            goto next;
        if (BPF_OP(code) == BPF_EXIT || BPF_OP(code) == BPF_CALL)
            goto next;
        //走到這裏的都是ebpf跳轉指令,i+insn[i].off+1爲跳轉目的區
        off = i + insn[i].off + 1;
        //如果跳轉範圍超過本跳轉指令所在函數的地址範圍,verify失敗
        if (off < subprog_start || off >= subprog_end) {e
            verbose(env, "jump out of range from insn %d to %d\n", i, off);
            return -EINVAL;
        }
next:
        //到達函數末尾,進行下一個函數
        if (i == subprog_end - 1) {
            /* to avoid fall-through from one subprog into another
             * the last insn of the subprog should be either exit
             * or unconditional jump back
             */
            if (code != (BPF_JMP | BPF_EXIT) &&
                code != (BPF_JMP | BPF_JA)) {
                verbose(env, "last insn is not an exit or jmp\n");
                return -EINVAL;
            }
            subprog_start = subprog_end;
            cur_subprog++;
            if (cur_subprog < env->subprog_cnt)
                subprog_end = subprog[cur_subprog + 1].start;
        }
    }
    return 0;
}

 check_subprogs函數的邏輯比較簡單,兩輪循環,首先找出所有ebpf函數的首地址(不包括內核helper func的調用),然後把每個函數的首地址,按照升序插入到env->subprog_info數組。第二輪循環,遍歷prog所有指令,確保所有的跳轉指令的跳轉範圍位於本函數地址範圍之內,否則失敗。
 再看第3步的check_cfg檢查bpf 主函數是否存在循環。
 這裏採用非遞歸深度優先算法探測程序是否是DAG(Directed acyclic graph 有向無環圖),即檢測程序不存在循環。
 借用網上的一張圖來看下。
dfs tree
 首先根據深度有優先算法將程序執行流轉換成一個tree,這個tree可能存在四種tree edge:
 tree edge:圖中所有的綠色箭頭都是正常的tree edge
 forward edge:1->8的黑色箭頭爲forward edge
 cross edge:5->4的黑色箭頭屬於cross edge
 back edge : 6->2的黑色箭頭爲back edge,檢測到back edge說明程序存在loop

static int check_cfg(struct bpf_verifier_env *env)
{
    struct bpf_insn *insns = env->prog->insnsi;
    int insn_cnt = env->prog->len;
    int ret = 0;
    int i, t;
    //申請insn_stat跟蹤指令的狀態
    insn_state = kcalloc(insn_cnt, sizeof(int), GFP_KERNEL);
    if (!insn_state)
        return -ENOMEM;
    //保存當前執行流的指令,供push和pop用
    insn_stack = kcalloc(insn_cnt, sizeof(int), GFP_KERNEL);
    if (!insn_stack) {
        kfree(insn_state);
        return -ENOMEM;
    }

    insn_state[0] = DISCOVERED; /* mark 1st insn as discovered */
    insn_stack[0] = 0; /* 0 is the first instruction */
    cur_stack = 1;
//主循環,包含指令入棧和指令退棧
peek_stack:
    if (cur_stack == 0)
        goto check_state;
    t = insn_stack[cur_stack - 1];//取的上次入棧的指令
    //函數調用和跳轉指令的class都是BPF_JMP
    if (BPF_CLASS(insns[t].code) == BPF_JMP) {
        u8 opcode = BPF_OP(insns[t].code);

        if (opcode == BPF_EXIT) {
            goto mark_explored;//遇到函數末尾,對執行過的指令進行explored標記,並退棧操作
        } else if (opcode == BPF_CALL) { //函數調用
            //push_insn對函數調用的下一條指令入棧,如果入棧成功,標記函數調用指令的insn_state
            //標記爲DISCOVERED和FALLTHROGH,下一條指令爲DISCOVERED,並返回1。
            ret = push_insn(t, t + 1, FALLTHROUGH, env);
            if (ret == 1)
                goto peek_stack;  //返回1,代表入入棧成功。跳轉到peek_stack,獲取本次入棧的指令,
                //並對下一條指令繼續入棧。
            else if (ret < 0)
                goto err_free;
            if (t + 1 < insn_cnt)
                env->explored_states[t + 1] = STATE_LIST_MARK;
            if (insns[t].src_reg == BPF_PSEUDO_CALL) {
                env->explored_states[t] = STATE_LIST_MARK;//對於bpf到bpf函數的調用,對callee函數
                //也需要進行push_insn來進行入棧,對於kernel helper的函數調用這一步不需要
                ret = push_insn(t, t + insns[t].imm + 1, BRANCH, env);
                if (ret == 1)
                    goto peek_stack;
                else if (ret < 0)
                    goto err_free;
            }
        } else if (opcode == BPF_JA) { //無條件跳轉,類似goto
            if (BPF_SRC(insns[t].code) != BPF_K) { //合法性檢查
                ret = -EINVAL;
                goto err_free;
            }
            /* unconditional jump with single edge */
            //無條件跳轉指令只需要建立到跳轉的分支的入棧操作
            //因爲永遠不會執行到下一條指令,沒必要模擬
            ret = push_insn(t, t + insns[t].off + 1,
                    FALLTHROUGH, env);
            if (ret == 1)
                goto peek_stack;
            else if (ret < 0)
                goto err_free;
            /* tell verifier to check for equivalent states
             * after every call and jump
             */
            if (t + 1 < insn_cnt)
                env->explored_states[t + 1] = STATE_LIST_MARK;
        } else {
            /* conditional jump with two edges */
            //條件跳轉兩個分支都需要入棧模擬執行,因爲兩個分支
            //都有可能執行到。這裏深度優先算法先搜索false分支
            env->explored_states[t] = STATE_LIST_MARK;
            //false分支
            ret = push_insn(t, t + 1, FALLTHROUGH, env);
            if (ret == 1)
                goto peek_stack;
            else if (ret < 0)
                goto err_free;
            //true分支
            ret = push_insn(t, t + insns[t].off + 1, BRANCH, env);
            if (ret == 1)
                goto peek_stack;
            else if (ret < 0)
                goto err_free;
        }
    } else {
        /* all other non-branch instructions with single
         * fall-through edge
         */ 
         //正常指令的入棧
        ret = push_insn(t, t + 1, FALLTHROUGH, env);
        if (ret == 1)
            goto peek_stack;
        else if (ret < 0)
            goto err_free;
    }
//遇到BPF_EXIT指令後,會觸發一些列的退棧操作,會把每個
//退棧的指令標記爲explored
mark_explored:
    insn_state[t] = EXPLORED;
    if (cur_stack-- <= 0) { //退棧
        verbose(env, "pop stack internal bug\n");
        ret = -EFAULT;
        goto err_free;
    }
    goto peek_stack;
//入棧之後在退棧,退棧完畢之後,纔會執行到這裏,也就是
//整個深度優先搜索完成
check_state:
	//檢查所有指令是否被執行過,如果沒有,返回-EINVAL
	//進而會導致整個bpf verify流程失敗
    for (i = 0; i < insn_cnt; i++) {
        if (insn_state[i] != EXPLORED) {
            verbose(env, "unreachable insn %d\n", i);
            ret = -EINVAL;
            goto err_free;
        }
    }
    ret = 0; /* cfg looks good */

err_free:
    kfree(insn_state);
    kfree(insn_stack);
    return ret;
}                                                                                                                                                                                                                                                                                      

 push_insn函數負責指令入棧。t爲當前指令index,w爲下一條指令,也就是要入棧的指令。e可能是FALLTHROUGH代表當前指令到下一條指令是順序執行,e爲BRANCH代表w指令爲條件跳轉指令。
 函數返回1,代表w成功入棧,當前爲入棧過程。函數返回0,w不會入棧,當前爲退棧流程。返回-EINVAl,代表跳轉超出整個prog程序或者檢測到loop。

static int push_insn(int t, int w, int e, struct bpf_verifier_env *env)
{
	//退棧時候遇到tree edge的指令
    if (e == FALLTHROUGH && insn_state[t] >= (DISCOVERED | FALLTHROUGH))
        return 0;
    //退棧時候遇到分支跳轉指令
    if (e == BRANCH && insn_state[t] >= (DISCOVERED | BRANCH))
        return 0;
    //跳轉超出prog程序的範圍
    if (w < 0 || w >= env->prog->len) {
        verbose_linfo(env, t, "%d: ", t);
        verbose(env, "jump out of range from insn %d to %d\n", t, w);
        return -EINVAL;
    }

    if (e == BRANCH)
        /* mark branch target for state pruning */
        env->explored_states[w] = STATE_LIST_MARK;
    //入棧操作
    if (insn_state[w] == 0) {
        /* tree-edge */
        insn_state[t] = DISCOVERED | e;
        insn_state[w] = DISCOVERED;
        if (cur_stack >= env->prog->len)
            return -E2BIG;
        insn_stack[cur_stack++] = w; //下條指令入棧
        return 1;
        //如果將要入棧的指令已經在棧中,出現loop
    } else if ((insn_state[w] & 0xF0) == DISCOVERED) {
        verbose_linfo(env, t, "%d: ", t);
        verbose_linfo(env, w, "%d: ", w);
        verbose(env, "back-edge from insn %d to %d\n", t, w);
        return -EINVAL;
      //將要執行的指令被執行過,但不在棧中,出現那麼就是forward-edge或者cross-edge,
      //這種情況不構成loop。需要注意的是,這種代表下一條指令已經被執行過。不必在對這條指令壓棧
      //因爲之前的檢查已經模擬執行過,沒有問題。遇到這種情況相當於BPF_EXIT,可以觸發退棧操作。
    } else if (insn_state[w] == EXPLORED) {   
        /* forward- or cross-edge */
        insn_state[t] = DISCOVERED | e;
    } else {
        verbose(env, "insn state internal bug\n");
        return -EFAULT;
    }
    return 0;
}

 check_cfg從prog第一條指令開始進行模擬執行,遇到函數調用先執行函數後的程序,再執行函數體,遇到條件跳轉,先false分支,在true分支。因爲是深度優先,每個分支的指令都會進行入棧,並執行到底,直到遇到BPF_EXIT指令。遇到BPF_EXIT開始退棧操作,遇到分支,在深度搜索相鄰分支。
 每次退棧操作會調用push_insn函數嘗試把退棧指令下一條指令入棧,對於非分支或者非調用指令,因爲指令已經執行過,push_insn指令會返回0,會繼續退棧。如果遇到分支處理指令,對於已經處理分支,push_insn返回0,會繼續把另一分支指令入棧,push_insn返回1,入棧成功,這時候會進入另外分支的深度遍歷操作。push_insn還會檢測到超過prog程序範圍跳轉,會返回-EINVAL。
 入棧指令會標記爲DISCOVERD,代表指令在棧中,每次退棧之後,指令被標記位EXPLORED,代表該指令被執行過,但不在棧中。如果當前執行的指令的下一條指令是正在棧中(DISCOVERED狀態),檢測到loop。
 在函數最後,深度優先搜索完畢,如果有指令不是EXPLORED,說明沒有被執行到過,verify失敗。
 最後,check_cfq函數同樣可以檢測函數遞歸的情況,遞歸調用是種特殊的back-edge。

 看第3步的do_check函數,這個函數比較長,不在貼代碼,只是根據自己理解,說下流程。
 do_check函數遍歷所有prog的指令,主要用於檢測指令的訪問權限的合法性。
 do_check函數的框架,主要是將每個跳轉分支爲一級維度進行遍歷,遇到分支,先處理本分支本分支,other分支入棧,同時在分支處理過程中,如果遇到函數調用,在以函數調用爲二級維度對函數返回地址進行壓棧。函數執行完也就是遇到BPF_EXIT指令,執行函數返回處理,如果本分支的所有函數都已出棧,那麼遇到BPF_EXIT指令,會出棧本分支的other分支,繼續處理。
 do_check以函數調用維度來維護10寄存和本函數所有棧內存器的訪問權限和狀態。記錄到strut bpf_func_state:

struct bpf_func_state {
     struct bpf_reg_state regs[MAX_BPF_REG]; //10個寄存器的訪問權限和liveness
     int callsite; //當前流程調用本函數的調用指令index
     u32 frameno; //當前函數調用深度
     u32 subprogno; //本函數在env->subprog_info中的index
     int acquired_refs;
     struct bpf_reference_state *refs;
     int allocated_stack; //當前函數的棧的最大消耗
     struct bpf_stack_state *stack; //本函數所有棧的訪問權限和狀態
 };

對於bpf_reg_state的type有以下狀態:

enum bpf_reg_type {
    NOT_INIT = 0,            /* 寄存器包含無效值,不允許read*/
    SCALAR_VALUE,            /* 寄存器包含有效值,但不是有效指針*/
    PTR_TO_CTX,              /* 指向struct bpf_context */
    CONST_PTR_TO_MAP,        /* 指向struct bpf_map */
    PTR_TO_MAP_VALUE,        /* 指向 map element value */
    PTR_TO_MAP_VALUE_OR_NULL,/* 指向map elem value 或者null */
    PTR_TO_STACK,            /* 指向bpf棧數據 */
    PTR_TO_PACKET_META,      /* 指向skb->data - meta_len */
    PTR_TO_PACKET,           /* 指向to skb->data */
    PTR_TO_PACKET_END,       /* 指向skb->data + headlen */
    PTR_TO_FLOW_KEYS,        /* 指向bpf_flow_keys */
    PTR_TO_SOCKET,           /* 指向struct bpf_sock */
    PTR_TO_SOCKET_OR_NULL,   /* 指向struct bpf_sock*/
};

 對於bpf_stack_state有以下幾種類型:

enum bpf_stack_slot_type {
    STACK_INVALID,    /* 棧slot無有效數據*/
    STACK_SPILL,      /* spill類型指針 */
    STACK_MISC,   /* 包含有效數據,但不是合法指針類型*/
    STACK_ZERO,   /* 爲常數0*/
};

 其中STACK_SPILL類型的是棧slot保存瞭如下類型的合法指針:

  • CONST_PTR_TO_MAP, /* 指向struct bpf_map */
  • PTR_TO_MAP_VALUE, /* 指向 map element value */
  • PTR_TO_MAP_VALUE_OR_NULL,/* 指向map elem value 或者null */
  • PTR_TO_STACK, /* 指向bpf棧數據 */
  • PTR_TO_PACKET_META, /* 指向skb->data - meta_len */
  • PTR_TO_PACKET, /* 指向to skb->data */
  • PTR_TO_PACKET_END, /* 指向skb->data + headlen */
  • PTR_TO_FLOW_KEYS, /* 指向bpf_flow_keys */
  • PTR_TO_SOCKET, /* 指向struct bpf_sock */

 以PTR_TO_STACK類型的數據,也就是棧數據的訪問類型權限檢查爲例看下BPF_LDX和BPF_STX兩類指令,分別是讀寫棧內存的指令。
 BPF_LDX是bpf的64位讀內存指令,假設讀的是棧內存。此時insn->src_reg爲BPF_REG_10,insn->src_reg->off是指源寄存器內容相對棧幀寄存器的偏移,insn->off代表本次load要從insn->src_reg指向的區域的多大偏移開始讀。do_check開始會調用init_reg_state函數對所有寄存器類型最初始化。看下這個函數:

 static void init_reg_state(struct bpf_verifier_env *env,
                struct bpf_func_state *state)
 {
     struct bpf_reg_state *regs = state->regs;
     int i;
 
     for (i = 0; i < MAX_BPF_REG; i++) {
         mark_reg_not_init(env, regs, i); //設置10個寄存器的類型位NOT_INIT,全部不可讀
         regs[i].live = REG_LIVE_NONE;
         regs[i].parent = NULL;
     }
 
     /*設置棧幀寄存器類型爲PTR_TO_STACK*/
     regs[BPF_REG_FP].type = PTR_TO_STACK;
     mark_reg_known_zero(env, regs, BPF_REG_FP);
     regs[BPF_REG_FP].frameno = state->frameno;
 
     /* 設置prog第一個參數R1寄存器類型爲PTR_TO_CTX,也就是pt_regs結構體 */
     regs[BPF_REG_1].type = PTR_TO_CTX;
     mark_reg_known_zero(env, regs, BPF_REG_1);
 }

 可見,在剛開始,只有棧幀寄存器和prog第一個入參R1是可讀的。
 do_check函數遍歷到BPF_LDX指令的時候,首先調用 check_reg_arg(env, insn->src_reg, SRC_OP)檢查R10寄存器是否可讀,看它是否是NOT_INIT狀態,是的話代表不可讀,返回失敗,這裏R10寄存器爲PTR_TO_STACK,check_reg_arg返回成功。
 然後調用check_mem_access函數檢查,具體要讀的棧數據是否可讀。看下這個函數,只看PTR_TO_STACK檢查相關的:

static int check_mem_access(struct bpf_verifier_env *env, int insn_idx, u32 regno,
                int off, int bpf_size, enum bpf_access_type t,
                int value_regno, bool strict_alignment_once)
{
    struct bpf_reg_state *regs = cur_regs(env);
    struct bpf_reg_state *reg = regs + regno;
    struct bpf_func_state *state;
    int size, err = 0;

    size = bpf_size_to_bytes(bpf_size);
    if (size < 0)
        return size;

    /* alignment checks will add in reg->off themselves */
    err = check_ptr_alignment(env, reg, off, size, strict_alignment_once);
    if (err)
        return err;
    ......
    if (reg->type == PTR_TO_STACK) {
        off += reg->var_off.value;
        err = check_stack_access(env, reg, off, size);
        if (err)
            return err;

        state = func(env, reg);
        err = update_stack_depth(env, state, off);
        if (err)
            return err;

        if (t == BPF_WRITE)
            err = check_stack_write(env, state, off, size,
                        value_regno, insn_idx);
        else
            err = check_stack_read(env, state, off, size,
        }
        ......
    }

 首先調用check_ptr_alignment函數檢查指針的對齊訪問,不對齊不被允許。

 分別看下,check_stack_access函數、check_stack_depth函數和check_stack_read函數:

static int check_stack_access(struct bpf_verifier_env *env,
                  const struct bpf_reg_state *reg,
                  int off, int size)
{
    //對棧的訪問,偏移必須是常量固定的,不能是指針
    if (!tnum_is_const(reg->var_off)) {
        char tn_buf[48];

        tnum_strn(tn_buf, sizeof(tn_buf), reg->var_off);
        verbose(env, "variable stack access var_off=%s off=%d size=%d",
            tn_buf, off, size);
        return -EACCES;
    }
    //棧是遞減棧,off必須小於等於零,並且絕對值偏移需小於512字節(prog最大棧允許)
    if (off >= 0 || off < -MAX_BPF_STACK) {
        verbose(env, "invalid stack off=%d size=%d\n", off, size);
        return -EACCES;
    }

    return 0;
}

 check_stack_access函數主要檢查偏移是否爲固定偏移,並看棧偏移否超過最大棧大小(512字節)。


static int update_stack_depth(struct bpf_verifier_env *env,
                  const struct bpf_func_state *func,
                  int off)
{
    u16 stack = env->subprog_info[func->subprogno].stack_depth;

    if (stack >= -off)
        return 0;

    /* 如果這次對棧的訪問地址超出本函數目前的棧消耗,那麼擴充之*/
    env->subprog_info[func->subprogno].stack_depth = -off;
    return 0;
}

 這個函數維護了每個函數對棧的最大消耗,並記錄到subprog_info數組裏相應函數的stack_depth裏,這個值在check_max_stack_depth函數模擬函數調用裏會用來判斷棧的當前所有棧消耗是否超標。

 看下check_stack_read函數:

static int check_stack_read(struct bpf_verifier_env *env,
                struct bpf_func_state *reg_state /* func where register points to */,
                int off, int size, int value_regno)
{
    struct bpf_verifier_state *vstate = env->cur_state;
    struct bpf_func_state *state = vstate->frame[vstate->curframe];
    int i, slot = -off - 1, spi = slot / BPF_REG_SIZE;
    u8 *stype;
    //這裏的off是insn->off和insn->src_reg->off的和,如果棧訪問地址超過本次函數的棧最大用度,非法
    if (reg_state->allocated_stack <= slot) {
        verbose(env, "invalid read from stack off %d+0 size %d\n",
            off, size);
        return -EACCES;
    }
    stype = reg_state->stack[spi].slot_type;

    if (stype[0] == STACK_SPILL) {
        //如果STACK_SPILL是指的是棧裏保存的是bpf幾種類型的合法指針,如果部分讀取,失敗
        if (size != BPF_REG_SIZE) {
            verbose(env, "invalid size of register spill\n");
            return -EACCES;
        }
        //對於STACK_SPILL類型的,8個slot所有類型必須都是STACK_SPILL
        for (i = 1; i < BPF_REG_SIZE; i++) {
            if (stype[(slot - i) % BPF_REG_SIZE] != STACK_SPILL) {
                verbose(env, "corrupted spill memory\n");
                return -EACCES;
            }
        }

        if (value_regno >= 0) {
            //檢查成功後,更新src_dest的reg state到dst reg
            state->regs[value_regno] = reg_state->stack[spi].spilled_ptr;
          
            state->regs[value_regno].live |= REG_LIVE_WRITTEN;
        }
        mark_reg_read(env, &reg_state->stack[spi].spilled_ptr,
                  reg_state->stack[spi].spilled_ptr.parent);
        return 0;
    } else {
        int zeros = 0;
        //對於非STACK_SPILL類型的,以次檢測每個要讀取的字節對應的slot,
        //size要讀取的字節數,可以小於8,要求每個要讀取的字節要麼STACK_MISC,要麼是0,
        //如果是STACK_INVALID,不可讀,失敗
        for (i = 0; i < size; i++) {
            if (stype[(slot - i) % BPF_REG_SIZE] == STACK_MISC)
                continue;
            if (stype[(slot - i) % BPF_REG_SIZE] == STACK_ZERO) {
                zeros++;
                continue;
           }
            verbose(env, "invalid read from stack off %d+%d size %d\n",
                off, i, size);
            return -EACCES;
        }
        mark_reg_read(env, &reg_state->stack[spi].spilled_ptr,
                  reg_state->stack[spi].spilled_ptr.parent);
        if (value_regno >= 0) {
            if (zeros == size) {
                //如果所有讀取的字節均是0,標記dst寄存器值爲0,類型爲SCALAR_VALUE
                __mark_reg_const_zero(&state->regs[value_regno]);
            } else {
                //如果讀取的數據不全爲0,標記dst寄存器類型爲SCALAR_VALUE。
                mark_reg_unknown(env, state->regs, value_regno);
            }
            state->regs[value_regno].live |= REG_LIVE_WRITTEN;
        }
        return 0;
    }
}

 check_stack_read函數從以下幾個方面進行讀權限檢查:

  • 棧偏移不能超過當前函數最大棧消耗
  • 如果棧內存類型位STACK_SPILL類型,那麼不支持非對齊當問和部分讀取,並且棧內存所在的8個字節全部應該是STACK_SPILLl類型。
  • 如果是非STACK_SPILL類型,需要確保要讀取的棧數據必須要麼是TACK_MISC類型或者STACK_ZERO類型,或者兼有二者。非STACK_SPILL類型的棧支持部分讀取。最後設置dst寄存器爲SCALER_VALUE類型。
     最後需要注意,bpf_stack_state中slot_type數組低地址保存的實際棧數據高地址棧數據的訪問類型

 對於BPF_STX指令,如果目的寄存器爲BPF_REG_10,那麼需要調用check_stack_write函數類檢查棧內存的可寫權限:

static int check_stack_write(struct bpf_verifier_env *env,
                 struct bpf_func_state *state, /* func where register points to */
                 int off, int size, int value_regno, int insn_idx)
{
    struct bpf_func_state *cur; /* state of the current function */
    int i, slot = -off - 1, spi = slot / BPF_REG_SIZE, err;
    enum bpf_reg_type type;
    //如果這次對棧內存的寫導致棧擴展,那麼需要重新申請bpf_stack_state內存,
    //因爲棧變大,用於跟蹤棧數據狀態的內存不夠了
    err = realloc_func_state(state, round_up(slot + 1, BPF_REG_SIZE),
                 state->acquired_refs, true);
    if (err)
        return err;
    //如果棧數據保存的合法的指針類型,但這次寫是部分寫,這會寫壞指針,不允許非特權用戶這麼做
    if (!env->allow_ptr_leaks &&
        state->stack[spi].slot_type[0] == STACK_SPILL &&
        size != BPF_REG_SIZE) {
        verbose(env, "attempt to corrupt spilled pointer on stack\n");
        return -EACCES;
    }
    
    cur = env->cur_state->frame[env->cur_state->curframe];
    if (value_regno >= 0 &&
        is_spillable_regtype((type = cur->regs[value_regno].type))) {
         
        //對於源寄存器是STACK_SPILL的情況,部分寫不允許
        if (size != BPF_REG_SIZE) {
            verbose(env, "invalid size of register spill\n");
            return -EACCES;
        }

        if (state != cur && type == PTR_TO_STACK) {
            verbose(env, "cannot spill pointers to stack into stack frame of the caller\n");
            return -EINVAL;
        }

        /* save register state */
        state->stack[spi].spilled_ptr = cur->regs[value_regno];
        state->stack[spi].spilled_ptr.live |= REG_LIVE_WRITTEN;

        for (i = 0; i < BPF_REG_SIZE; i++) {
            if (state->stack[spi].slot_type[i] == STACK_MISC &&
                !env->allow_ptr_leaks) {
                int *poff = &env->insn_aux_data[insn_idx].sanitize_stack_off;
                int soff = (-spi - 1) * BPF_REG_SIZE;
                
                if (*poff && *poff != soff) {
                    /* disallow programs where single insn stores
                     * into two different stack slots, since verifier
                     * cannot sanitize them
                     */
                    verbose(env,
                        "insn %d cannot access two stack slots fp%d and fp%d",
                        insn_idx, *poff, soff);
                    return -EINVAL;
                }
                *poff = soff;
            }
            //源寄存包含是合法的bpf指針類型,設置棧數據類型爲STACK_SPILL類型
            state->stack[spi].slot_type[i] = STACK_SPILL;
        }
    } else { //源寄存器包含是非有效指針的情況
        u8 type = STACK_MISC;
        
        state->stack[spi].spilled_ptr.type = NOT_INIT;
        //如果棧內存是是STACK_SPILL類型,因爲寫入不是指針,所以需要設置爲STACK_MISC
        //代表棧包含非指針、非零變量,這前面不矛盾,走到這裏是特權用戶
        if (state->stack[spi].slot_type[0] == STACK_SPILL)
            for (i = 0; i < BPF_REG_SIZE; i++)
                state->stack[spi].slot_type[i] = STACK_MISC;

        /* only mark the slot as written if all 8 bytes were written
         * otherwise read propagation may incorrectly stop too soon
         * when stack slots are partially written.
         * This heuristic means that read propagation will be
         * conservative, since it will add reg_live_read marks
         * to stack slots all the way to first state when programs
         * writes+reads less than 8 bytes
         */
        if (size == BPF_REG_SIZE)
            state->stack[spi].spilled_ptr.live |= REG_LIVE_WRITTEN;

        
        if (value_regno >= 0 &&
            register_is_null(&cur->regs[value_regno]))
            type = STACK_ZERO;

        //如果源寄存器爲0,標記所寫棧slot的類型位STACK_ZERO,否則STACK_MISC
        for (i = 0; i < size; i++)
            state->stack[spi].slot_type[(slot - i) % BPF_REG_SIZE] =
                type;
    }
    return 0;
}

 check_stack_write函數對棧內存進行以下檢查:

  • 如果此次寫超過本函數的目前最大用度(allocated_stack),需要重新申請bpf_stack_state內存,bpf_stack_state需要記錄每個棧數據的訪問權限,內存不夠了
  • 對於非特權用戶,不允許部分寫STACK_SPILL類型的棧數據
  • 對於源寄存器是STACK_SPILL的情況,並且是部分寫棧數據,不被允許。8字節全寫的話,最後需要標記棧數據8個字節全部爲STACK_SPILL類型
  • 對於源寄存器是非STACK_SPILL的情況,根據源寄存器和寫入字節的情況設置棧數據相應的slot類型爲STACK_ZERO或者STACK_MISC,非STACK_SPILL的情況支持部分寫的
     最後需要注意的是,bpf_stack_state中slot_type數組低地址保存的實際棧數據高地址棧數據的訪問類型

 看下第5步的check_max_stack_depth函數:

static int check_max_stack_depth(struct bpf_verifier_env *env)
{
    int depth = 0, frame = 0, idx = 0, i = 0, subprog_end;
    struct bpf_subprog_info *subprog = env->subprog_info;
    struct bpf_insn *insn = env->prog->insnsi;
    int ret_insn[MAX_CALL_FRAMES]; //函數返回地址
    int ret_prog[MAX_CALL_FRAMES]; //保存返回函數的idx

process_func:
    /* round up to 32-bytes, since this is granularity
     * of interpreter stack size
     */
    depth += round_up(max_t(u32, subprog[idx].stack_depth, 1), 32); //stack_depth是本層函數對棧最大的消耗
    if (depth > MAX_BPF_STACK) {  //超過MAX_BPF_STACK(512),verify失敗
        verbose(env, "combined stack size of %d calls is %d. Too large\n",
            frame + 1, depth);
        return -EACCES;
    }
//取下一個函數。subprog[0]是主函數
continue_func:
    subprog_end = subprog[idx + 1].start;
    for (; i < subprog_end; i++) {
        if (insn[i].code != (BPF_JMP | BPF_CALL))
            continue;
        if (insn[i].src_reg != BPF_PSEUDO_CALL)
            continue;
        //只有BPF之間函數調用指令纔會走到這裏
        /* remember insn and function to return to */
        ret_insn[frame] = i + 1;  //函數調用返回地址
        ret_prog[frame] = idx;    //當前函數idx

        /* find the callee */
        i = i + insn[i].imm + 1; //調用函數callee的地址
        idx = find_subprog(env, i); //根據callee地址查找callee的idx
        if (idx < 0) {
            WARN_ONCE(1, "verifier bug. No program starts at insn %d\n",
                  i);
            return -EFAULT;
        }
        frame++;
        if (frame >= MAX_CALL_FRAMES) { //函數調用深度大於8,verify失敗
            WARN_ONCE(1, "verifier bug. Call stack is too deep\n");
            return -EFAULT;
        }
        goto process_func;
    }
    /* end of for() loop means the last insn of the 'subprog'
     * was reached. Doesn't matter whether it was JA or EXIT
     */
     走到這裏,當前函數已經執行完了,函數需要返回上一層
    if (frame == 0)
        return 0;
    depth -= round_up(max_t(u32, subprog[idx].stack_depth, 1), 32); 
    frame--;
    i = ret_insn[frame]; //彈出函數返回地址
    idx = ret_prog[frame]; //彈出上層函數的idx
    goto continue_func;   //返回上層函數
}

 函數裏已經註釋,該函數遍歷prog的指令,只關注函數調用,模擬所有函數調用,檢查函數調用深度是否超過 MAX_CALL_FRAMES(8),並檢查棧的消耗是否超過MAX_BPF_STACK(512)。
 看下第6步的fixup_bpf_calls函數:

static int fixup_bpf_calls(struct bpf_verifier_env *env)
{
    struct bpf_prog *prog = env->prog;
    struct bpf_insn *insn = prog->insnsi;
    const struct bpf_func_proto *fn;
    const int insn_cnt = prog->len;
    const struct bpf_map_ops *ops;
    struct bpf_insn_aux_data *aux;
    struct bpf_insn insn_buf[16];
    struct bpf_prog *new_prog;
    struct bpf_map *map_ptr;
    int i, cnt, delta = 0;

    for (i = 0; i < insn_cnt; i++, insn++) {
        if (insn->code == (BPF_ALU64 | BPF_MOD | BPF_X) ||
            insn->code == (BPF_ALU64 | BPF_DIV | BPF_X) ||
            insn->code == (BPF_ALU | BPF_MOD | BPF_X) ||
            insn->code == (BPF_ALU | BPF_DIV | BPF_X)) {
            bool is64 = BPF_CLASS(insn->code) == BPF_ALU64;
            struct bpf_insn mask_and_div[] = {
                BPF_MOV32_REG(insn->src_reg, insn->src_reg),
                /* Rx div 0 -> 0 */
                BPF_JMP_IMM(BPF_JNE, insn->src_reg, 0, 2),
                BPF_ALU32_REG(BPF_XOR, insn->dst_reg, insn->dst_reg),
                BPF_JMP_IMM(BPF_JA, 0, 0, 1),
                *insn,
            };
            struct bpf_insn mask_and_mod[] = {
                BPF_MOV32_REG(insn->src_reg, insn->src_reg),
                /* Rx mod 0 -> Rx */
                BPF_JMP_IMM(BPF_JEQ, insn->src_reg, 0, 1),
                *insn,
            };
            struct bpf_insn *patchlet;

            if (insn->code == (BPF_ALU64 | BPF_DIV | BPF_X) ||
                insn->code == (BPF_ALU | BPF_DIV | BPF_X)) {
                patchlet = mask_and_div + (is64 ? 1 : 0);
                cnt = ARRAY_SIZE(mask_and_div) - (is64 ? 1 : 0);
            } else {
                patchlet = mask_and_mod + (is64 ? 1 : 0);
                cnt = ARRAY_SIZE(mask_and_mod) - (is64 ? 1 : 0);
            }

            new_prog = bpf_patch_insn_data(env, i + delta, patchlet, cnt);
            if (!new_prog)
                return -ENOMEM;

            delta    += cnt - 1;
            env->prog = prog = new_prog;
            insn      = new_prog->insnsi + i + delta;
            continue;
        }
.....................................................................................................

            switch (insn->imm) {
            case BPF_FUNC_map_lookup_elem:
                insn->imm = BPF_CAST_CALL(ops->map_lookup_elem) -
                        __bpf_call_base;
                continue;
            case BPF_FUNC_map_update_elem:
                insn->imm = BPF_CAST_CALL(ops->map_update_elem) -
                        __bpf_call_base;
                continue;
            case BPF_FUNC_map_delete_elem:
                insn->imm = BPF_CAST_CALL(ops->map_delete_elem) -
                        __bpf_call_base;
                continue;
            case BPF_FUNC_map_push_elem:
                insn->imm = BPF_CAST_CALL(ops->map_push_elem) -
                        __bpf_call_base;
                continue;
            case BPF_FUNC_map_pop_elem:
                insn->imm = BPF_CAST_CALL(ops->map_pop_elem) -
                        __bpf_call_base;
                continue;
            case BPF_FUNC_map_peek_elem:
                insn->imm = BPF_CAST_CALL(ops->map_peek_elem) -
                        __bpf_call_base;
                continue;
            }

            goto patch_call_imm;
        }

patch_call_imm:
        fn = env->ops->get_func_proto(insn->imm, env->prog);
        /* all functions that have prototype and verifier allowed
         * programs to call them, must be real in-kernel functions
         */
        if (!fn->func) {
            verbose(env,
                "kernel subsystem misconfigured func %s#%d\n",
                func_id_name(insn->imm), insn->imm);
            return -EFAULT;
        }
        insn->imm = fn->func - __bpf_call_base;
    }

                                                                                                                                               6845,21-24    94%

 這個函數主要處理兩個問題,一個BPF_MOD和BPF_DIV的除0問題,一個是修正BPF_CALL指令的跳轉距離的問題。
 20-26 行,將一個BPF_DIV指令擴展爲4個指令,如果源操作位0,dst置0,調到本指令的下一行執行,否則執行原指令。
 28-32行,將一個BPF_MOD指令擴展爲兩個指令,如果源操作數爲零,目的操作數保持不變,調到原指令下一行執行,否則執行原指令。
 45-52行,將擴展的指令替換原指令,如果有必要需要重新申請prog空間。
 55-100行,用來修正BPF_CALL指令的跳轉距離。ebpf訪問內核數據結構是受限的,需要通過調用相應的helper func來完成。ebpf指令格式如下:

 struct bpf_insn {
      __u8    code;       /* opcode */
      __u8    dst_reg:4;  /* dest register */
      __u8    src_reg:4;  /* source register */
      __s16   off;        /* signed offset */
      __s32   imm;        /* signed immediate constant */
  };

 ebpf指令類似RISC,除了BPF_LD_IMM64指令是16字節之外,其餘每個指令8個字節,對於BPF_CALL指令來說imm就是調用函數到本指令的距離。
 根據imm中的func id找到內核中對應的BPF_FUNC開頭的helper func函數,然後把該函數地址減去內核符號__bpf_call_base的地址得出新的offset,結果在寫到bpf_insn的imm裏,完成BPF_CALL指令的修正。這個明顯不是正確的offset,正確的offset應該是目標函數和本條指令地址的距離,真正的修正是在JIT裏。
 通過一個bpftrace例子來看下。

#include <linux/fs.h>
#include <linux/path.h>
#include <linux/blk_types.h>
#include <linux/sched.h>

BEGIN
{
    printf("Tracing dcache lookups... Hit Ctrl-C to end.\n");
    printf("%-8s %-6s %-16s %1s %s\n", "TIME", "PID", "COMM", "T", "FILE");
}

// comment out this block to avoid showing hits:
kprobe:vfs_write
{
    $file = (struct file *)arg0;
    printf("ino %ld!\n",$file->f_inode->i_ino);
}

 通過kprobe探測通過vfs_write函數,打印文件的ino。運行一下。執行,

[root@111-11-11-11 my_examples]# bpftool perf
pid 27290  fd 4: prog_id 19  uprobe  filename /proc/self/exe  offset 1935000
pid 27290  fd 63: prog_id 20  kprobe  func vfs_write  offset 0

bpftool查看下ebpf的prog的彙編

[root@11-11-11-11 my_examples]# bpftool prog dump xlate id 20 
   0: (bf) r6 = r1          //r1爲pt_regs機構體
   1: (79) r3 = *(u64 *)(r6 +112) //   r3 = regs->di,strut file地址
   2: (b7) r1 = 2 
   3: (7b) *(u64 *)(r10 -24) = r1
   4: (07) r3 += 32         // r3 = file->inode,參數3
   5: (bf) r1 = r10    
   6: (07) r1 += -8          //棧局部變量 (r10-8) 參數1
   7: (b7) r2 = 8            //拷貝字節  參數2
   8: (85) call bpf_probe_read#-46816 
   9: (79) r3 = *(u64 *)(r10 -8)    //(r10-8)存放bpf_probe_read讀取的file->inode副本結果,r3=inode
  10: (07) r3 += 64      //r3=inode->i_ino地址
  11: (bf) r1 = r10      
  12: (07) r1 += -8      //(r10-8)棧局部變量
  13: (b7) r2 = 8        //拷貝字節 參數2
  14: (85) call bpf_probe_read#-46816
  15: (79) r1 = *(u64 *)(r10 -8)    //r1爲讀取的inode->ino
  16: (7b) *(u64 *)(r10 -16) = r1 
  17: (18) r7 = map[id:6]   
  19: (85) call bpf_get_smp_processor_id#76416  //這是一個無效指令,供上條指令擴展填充,擴展後imm存map地址低32位
  20: (bf) r4 = r10
  21: (07) r4 += -24    //參數4 
  22: (bf) r1 = r6      // 參數1 r1=struct ptr_reg 
  23: (bf) r2 = r7      // 參數2 r2 = map地址
  24: (bf) r3 = r0      //參數3  flag
  25: (b7) r5 = 16      // 參數4 輸出拷貝字節,ino是8字節,這裏多拷貝前8個字節
  26: (85) call bpf_perf_event_output#-45376
  27: (b7) r0 = 0
  28: (95) exit

 第8、14和26行,爲ebpf調用內核helper 函數,#後面的數字經驗證等於helper func的地址減去__bpf_call_base的差值。在llvm和clang編譯好之後,這的imm應該是helper func id ,在fixup_bpf_call裏然後根據func id找真正的helper func,最後把函數地址和__bpf_call_base的差值給imm。這個是在運行的bpf prog,可見是轉換過的。
 看下第7步的的fixup_call_args函數,該函數調用了jit_subprogs函數,直接看這個函數:

static int jit_subprogs(struct bpf_verifier_env *env)
{
    struct bpf_prog *prog = env->prog, **func, *tmp;
    int i, j, subprog_start, subprog_end = 0, len, subprog;
    struct bpf_insn *insn;
    void *old_bpf_func;
    int err;
    //prog只有一個bpf函數,不做處理
    if (env->subprog_cnt <= 1)
        return 0;
    //遍歷prog的指令,只處理bpf到bpf的調用情況
    for (i = 0, insn = prog->insnsi; i < prog->len; i++, insn++) {
        if (insn->code != (BPF_JMP | BPF_CALL) ||
            insn->src_reg != BPF_PSEUDO_CALL)
            continue;
        //根據調用地址找到被調用子程序的所在的subinfo數組的內的索引subprog,保存subprog到指令的insn->off裏
        subprog = find_subprog(env, i + insn->imm + 1);
        if (subprog < 0) {
            WARN_ONCE(1, "verifier bug. No program starts at insn %d\n",
                  i + insn->imm + 1);
            return -EFAULT;
        }
        insn->off = subprog;
        env->insn_aux_data[i].call_imm = insn->imm;
        insn->imm = 1;
    }

    err = bpf_prog_alloc_jited_linfo(prog);
    if (err)
        goto out_undo_insn;

    err = -ENOMEM;
    func = kcalloc(env->subprog_cnt, sizeof(prog), GFP_KERNEL);
    if (!func)
        goto out_undo_insn;
    //爲每個subprog申請內存並根據父prog對其進行初始化
    for (i = 0; i < env->subprog_cnt; i++) {
        subprog_start = subprog_end;   //subprog開始指令index
        subprog_end = env->subprog_info[i + 1].start; //subprog結束指令index
        
        len = subprog_end - subprog_start;  //subprog指令長度
        func[i] = bpf_prog_alloc(bpf_prog_size(len), GFP_USER); //申請struct bpf_prog結構體
        if (!func[i])
            goto out_free;        memcpy(func[i]->insnsi, &prog->insnsi[subprog_start],
               len * sizeof(struct bpf_insn));
        func[i]->type = prog->type; 
        func[i]->len = len;
        if (bpf_prog_calc_tag(func[i]))
            goto out_free;
        func[i]->is_func = 1; 
        func[i]->aux->func_idx = i;
       
        func[i]->aux->btf = prog->aux->btf;
        func[i]->aux->func_info = prog->aux->func_info;
       
        func[i]->aux->name[0] = 'F';
        func[i]->aux->stack_depth = env->subprog_info[i].stack_depth; //棧消耗
        func[i]->jit_requested = 1;    //需要jit
        func[i]->aux->linfo = prog->aux->linfo;
        func[i]->aux->nr_linfo = prog->aux->nr_linfo;
        func[i]->aux->jited_linfo = prog->aux->jited_linfo;
        func[i]->aux->linfo_idx = env->subprog_info[i].linfo_idx;
        func[i] = bpf_int_jit_compile(func[i]); //對該subprog進行jit,後面在看這個函數
        if (!func[i]->jited) {
            err = -ENOTSUPP;
            goto out_free;
        }
        cond_resched();
    }
    //修正bpf到bpf函數調用距離
    for (i = 0; i < env->subprog_cnt; i++) {
    for (i = 0; i < env->subprog_cnt; i++) {
        insn = func[i]->insnsi;
        for (j = 0; j < func[i]->len; j++, insn++) {
            if (insn->code != (BPF_JMP | BPF_CALL) ||
                insn->src_reg != BPF_PSEUDO_CALL)
                continue;
            //調用距離爲調用子函數到__bpf_call_base地址的距離,這個和調用內核helper func的統一。
            //這個距離明顯不是正確的,正確的調用距離位爲調用函數子函數到本指令的距離,這個會在bpf_int_jit_compile函數裏處理
            //因爲後面還會進入bpf_int_jit_compile函數
            subprog = insn->off;
            insn->imm = (u64 (*)(u64, u64, u64, u64, u64))
                func[subprog]->bpf_func -
                __bpf_call_base;
        }
        func[i]->aux->func = func;
        func[i]->aux->func_cnt = env->subprog_cnt;
    }
    for (i = 0; i < env->subprog_cnt; i++) {
        old_bpf_func = func[i]->bpf_func;
        tmp = bpf_int_jit_compile(func[i]); //這次調用主要是處理bpf到bpf函數調用的調用距離
        if (tmp != func[i] || func[i]->bpf_func != old_bpf_func) {
            verbose(env, "JIT doesn't support bpf-to-bpf calls\n");
            err = -ENOTSUPP;
            goto out_free;
        }
        cond_resched();
    }

    /* finally lock prog and jit images for all functions and
     * populate kallsysm
     */
    for (i = 0; i < env->subprog_cnt; i++) {
        bpf_prog_lock_ro(func[i]); //設置jit之後的每個子函數所在內存只讀
        bpf_prog_kallsyms_add(func[i]); //添加每個subprog->bpf_func到kallsyms
    }
    for (i = 0, insn = prog->insnsi; i < prog->len; i++, insn++) {
        if (insn->code != (BPF_JMP | BPF_CALL) ||
            insn->src_reg != BPF_PSEUDO_CALL)
            continue;
        insn->off = env->insn_aux_data[i].call_imm;
        subprog = find_subprog(env, i + insn->off + 1);
        insn->imm = subprog;
    }

    prog->jited = 1; //jit完成,後面進入bpf_int_jit_compile函數會直接返回。
    prog->bpf_func = func[0]->bpf_func; //prog在在內核裏的可執行函數的地址
    prog->aux->func = func;
    prog->aux->func_cnt = env->subprog_cnt;
    bpf_prog_free_unused_jited_linfo(prog);
    return 0;
out_free:
    for (i = 0; i < env->subprog_cnt; i++)
        if (func[i])
            bpf_jit_free(func[i]);
    kfree(func);
out_undo_insn:
    /* cleanup main prog to be interpreted */
    prog->jit_requested = 0;
    for (i = 0, insn = prog->insnsi; i < prog->len; i++, insn++) {
        if (insn->code != (BPF_JMP | BPF_CALL) ||
            insn->src_reg != BPF_PSEUDO_CALL)
            continue;
        insn->off = 0;
        insn->imm = env->insn_aux_data[i].call_imm;
    }
    bpf_prog_free_jited_linfo(prog);
    return err;
}

 jit_subprogs函數用作多bpf函數prog的jit。主要有三步:

  • .函數首先對prog的每個函數生成一個subprog,並初始化,然後調用bpf_int_jit_compile函數對每個subprog進行單獨jit。
  • .修正bpf到bpf調用指令的調用距離問題,修改爲callee函數地址到__bpf_call_base的差值,並把這個值放入insn->imm。
  • .最後在對每個subprog調用bpf_int_jit_compile函數進行jit,這次jit主要是最終修正bpf到bpf的跳轉距離,修正爲callee函數到本調用指令的距離。
     爲何要進行兩輪jit。我的理解是第一次jit的時候,如果後面的subprog的jit還沒有進行,雖然ebpf的和x64指令基本可以做到一對一翻譯,但ebpf指令定長,但x64指令不定長,那麼就不可能知道bpf函數調用的調用距離,也不知道每個call指令的確切地址,只有對每個函數體都要進行過Jit之後,這個時候每個subprog在內核中函數地址已經確定,每個bpf call指令的地址也已經確定,這裏在進行一輪jit完成對bpf到bpf函數調用指令的修正
     內核裏有兩個jit的路徑,一個上面這個,bpf_prog_load->bpf_check->fixup_bpf_args->jit_suprogs->bpf_int_jit_compile。另外一個路徑bpf_prog_load->bpf_prog_select_runtime->bpf_int_jit_compile。
     前者是多bpf函數的jit路徑,後者是單bpf函數prog的jit路徑。第一個路徑執行完prog->jited設置爲1,等進入進入第二個路徑在do_jit函數裏看到jited=1函數直接返回。

 看下關鍵的JIT的流程,bpf_prog_load中,bpf_check對prog驗證通過之後,執行bpf_prog_select_runtime函數執行jit,最後是在do_jit函數裏。

 static int do_jit(struct bpf_prog *bpf_prog, int *addrs, u8 *image,
           int oldproglen, struct jit_context *ctx)
 {
     struct bpf_insn *insn = bpf_prog->insnsi;
     int insn_cnt = bpf_prog->len;
     bool seen_exit = false;
     u8 temp[BPF_MAX_INSN_SIZE + BPF_INSN_SAFETY];
     int i, cnt = 0;
     int proglen = 0;
     u8 *prog = temp;
 
     emit_prologue(&prog, bpf_prog->aux->stack_depth,
               bpf_prog_was_classic(bpf_prog));
 
      for (i = 0; i < insn_cnt; i++, insn++) {
         const s32 imm32 = insn->imm;
         u32 dst_reg = insn->dst_reg;
         u32 src_reg = insn->src_reg;
         u8 b2 = 0, b3 = 0;
         s64 jmp_offset;
         u8 jmp_cond;
         int ilen;
         u8 *func;
 
         switch (insn->code) {
            /* ALU */
         case BPF_ALU | BPF_ADD | BPF_X:
         case BPF_ALU | BPF_SUB | BPF_X:
         case BPF_ALU | BPF_AND | BPF_X:
         case BPF_ALU | BPF_OR | BPF_X:
         case BPF_ALU | BPF_XOR | BPF_X:
         case BPF_ALU64 | BPF_ADD | BPF_X:
         case BPF_ALU64 | BPF_SUB | BPF_X:
         case BPF_ALU64 | BPF_AND | BPF_X:
         case BPF_ALU64 | BPF_OR | BPF_X:
         case BPF_ALU64 | BPF_XOR | BPF_X:
             switch (BPF_OP(insn->code)) {
             case BPF_ADD: b2 = 0x01; break;
             case BPF_SUB: b2 = 0x29; break;
             case BPF_AND: b2 = 0x21; break;
             case BPF_OR: b2 = 0x09; break;
             case BPF_XOR: b2 = 0x31; break;
             }
             if (BPF_CLASS(insn->code) == BPF_ALU64)
                 EMIT1(add_2mod(0x48, dst_reg, src_reg));
             else if (is_ereg(dst_reg) || is_ereg(src_reg))
                 EMIT1(add_2mod(0x40, dst_reg, src_reg));
              EMIT2(b2, add_2reg(0xC0, dst_reg, src_reg));
             break;
 
         case BPF_ALU64 | BPF_MOV | BPF_X:
         case BPF_ALU | BPF_MOV | BPF_X:
             emit_mov_reg(&prog,
                       BPF_CLASS(insn->code) == BPF_ALU64,
                      dst_reg, src_reg);
             break;
 
             /* neg dst */
         case BPF_ALU | BPF_NEG:
         case BPF_ALU64 | BPF_NEG:
              if (BPF_CLASS(insn->code) == BPF_ALU64)
                 EMIT1(add_1mod(0x48, dst_reg));
             else if (is_ereg(dst_reg))
                 EMIT1(add_1mod(0x40, dst_reg));
             EMIT2(0xF7, add_1reg(0xD8, dst_reg));
             break;
 
         case BPF_ALU | BPF_ADD | BPF_K:
         case BPF_ALU | BPF_SUB | BPF_K:
         case BPF_ALU | BPF_AND | BPF_K:
         case BPF_ALU | BPF_OR | BPF_K:
         case BPF_ALU | BPF_XOR | BPF_K:
         case BPF_ALU64 | BPF_ADD | BPF_K:
         case BPF_ALU64 | BPF_SUB | BPF_K:
         case BPF_ALU64 | BPF_AND | BPF_K:
         case BPF_ALU64 | BPF_OR | BPF_K:
         case BPF_ALU64 | BPF_XOR | BPF_K:
             if (BPF_CLASS(insn->code) == BPF_ALU64)
                 EMIT1(add_1mod(0x48, dst_reg));
             else if (is_ereg(dst_reg))
                  EMIT1(add_1mod(0x40, dst_reg));

              /*
              * b3 holds 'normal' opcode, b2 short form only valid
              * in case dst is eax/rax.
              */
             switch (BPF_OP(insn->code)) {
             case BPF_ADD:
 ....................................................................................................... 
             /* call */
         case BPF_JMP | BPF_CALL:
             func = (u8 *) __bpf_call_base + imm32;
             jmp_offset = func - (image + addrs[i]);
             if (!imm32 || !is_simm32(jmp_offset)) {
                 pr_err("unsupported BPF func %d addr %p image %p\n",
                        imm32, func, image);
                 return -EINVAL;
             }
             EMIT1_off32(0xE8, jmp_offset);
             break;
 
        case BPF_JMP | BPF_TAIL_CALL:
             emit_bpf_tail_call(&prog);
             break;
 
             /* cond jump */
         case BPF_JMP | BPF_JEQ | BPF_X:
         case BPF_JMP | BPF_JNE | BPF_X:
         case BPF_JMP | BPF_JGT | BPF_X:
         case BPF_JMP | BPF_JLT | BPF_X:
         case BPF_JMP | BPF_JGE | BPF_X:
         case BPF_JMP | BPF_JLE | BPF_X:
         case BPF_JMP | BPF_JSGT | BPF_X:
         case BPF_JMP | BPF_JSLT | BPF_X:
         case BPF_JMP | BPF_JSGE | BPF_X:
         case BPF_JMP | BPF_JSLE | BPF_X:
             /* cmp dst_reg, src_reg */
             EMIT3(add_2mod(0x48, dst_reg, src_reg), 0x39,
                    add_2reg(0xC0, dst_reg, src_reg));
             goto emit_cond_jmp;
 
         case BPF_JMP | BPF_JSET | BPF_X:
             /* test dst_reg, src_reg */
             EMIT3(add_2mod(0x48, dst_reg, src_reg), 0x85,
                   add_2reg(0xC0, dst_reg, src_reg));
             goto emit_cond_jmp;
 
         case BPF_JMP | BPF_JSET | BPF_K:
             /* test dst_reg, imm32 */
             EMIT1(add_1mod(0x48, dst_reg));
 ............................................................................................
         if (image) {
             if (unlikely(proglen + ilen > oldproglen)) {
                 pr_err("bpf_jit: fatal error\n");
                 return -EFAULT;
             }
             memcpy(image + proglen, temp, ilen);
         }
         proglen += ilen;
         addrs[i] = proglen;
         prog = temp;
     }
     return proglen;
 }                                                                                                                                                                                                                                                                                                                                                                                                                           

 do_jit主要完成以下工作:

  • 1.調用emit_prologue函數,由於ebpf指令沒有棧操作指令,需要構造函數執行開始的棧初始化環境,使之完全符合x64的abi規則。包括,bp push,bp初始化,以及保存需要被調用者需要保存的寄存器(r15、r14、r13和rbx)。
  • 2.對於普通指令,基本能根據opcode和ebpf和x64的寄存器映射一對一的進行指令翻譯。
  • 3.修正BPF_CALL的函數調用。BPF_CALL主要用於helper func的調用和bpf到bpf函數的調用,現在BPF_CALL的imm是func地址減去__bpf_call_base,加上__bpf_call_base 得到func的地址,然後用func地址減去當前指令的地址作爲新的imm。
  • 4.函數收場工作。遇到BPF_EXIT指令,代表bpf函數結束。從棧裏彈出先前保存的r15、r14、r13和rbx寄存器,添加leave指令平棧,添加ret返回指令。
  • 5.emit_bpf_tail_call處理是prog調用另外一個prog的函數的問題,ebpf的prog是可以複用的。在調用深度允許的情況下,直接跳轉到被調用用者的prog->bpf_func + prologue_size地址執行。
  • 6.do_jit執行完畢,prog->bpf_func存放編譯後的可執行代碼。

 do_jit函數只被bpf_int_jit_compile調用。看下bpf_init_jit_compile主要執行流程:

  • 1.首先申請addr[prog->len]數組,用於存儲相應的ebpf指令對應的x64指令的地址。初始化假設每個ebpf指令對應64字節長的x64指令。
  • 2.循環調用do_jit函數。對一個函數體進行jit一般需要多輪pass。這主要因爲跳轉指令,在進行jit的時候,如果是向後跳轉,無法知道確切的距離,因爲後面的指令還沒有生成,這裏只能按照第一步的假設,假設每個ebpf指令對應的x86指令佔64個字節長,這個明顯偏大,會導致生成x86的jmp指令的長度比實際的長。所以幾輪調用do_jit函數,並沒有保存指令編譯結果,只是把每個ebpf指令對應的x84指令的偏移地址記錄到addr數組裏,第一輪pass過後,每個指令的位置信息都會記錄到addr裏,但這個位置信息只是大致的,不是準確的,因爲jmp指令的跳轉是不對的,而且生成的jmp指令本身長度也可能不對(本來是短跳轉,現在可能是長跳轉),所以需要第二輪pass,每一輪pass,要生成的總的x86指令長度不斷收斂,位置信息越來越準確,直到最後x86總的長度不再收斂,addr位置信息就完全正確
  • 3.x86指令總長度不在收斂後,根據這個長度申請內存,給do_jit,進行最後一輪pass,保存所有翻譯後的x64指令。
    舉個簡單的例子,如果一個函數就包含一個跳轉指令。
     第一輪pass之後,在addr[]裏非跳轉指令的長度從假設的64字節收斂爲真實的x64指令長度,如果是向後跳轉,那麼跳轉指令的跳轉距離是不對,偏大,並且可能導致跳轉指令本身長度偏大。
     第二輪pass,由於所有非跳轉指令在第一輪的長度都收斂爲正確的長度,所以do_jit遇到跳轉指令的時候,此時生成的跳轉指令無論是跳轉距離和指令長度都正確。
     第三輪pass。如果第二輪pass對跳轉指令的修正導致jmp指令變短(例如從長跳轉變爲短跳),此時,需要進行第三輪pass。第三輪pass中,所有指令長度都不再變了。
     第四次調用do_jit函數。第三輪pass中,所有指令佈局不會在收斂。add[]記錄的所有指令佔位全部正確。bpf_int_jit_compile函數會爲do_jit申請內存,在do_jit裏會把每個翻譯的指令保存起來,完成最後的jit。

 看下前面bpftrce程序prog的jit後的彙編代碼:

[root@11-11-11-11 ~]# bpftool prog dump jit id 20
0xffffffffc03eb692:
   0:	push   %rbp
   1:	mov    %rsp,%rbp
   4:	sub    $0x40,%rsp
   b:	sub    $0x28,%rbp
   f:	mov    %rbx,0x0(%rbp)
  13:	mov    %r13,0x8(%rbp)
  17:	mov    %r14,0x10(%rbp)
  1b:	mov    %r15,0x18(%rbp)
  1f:	xor    %eax,%eax
  21:	mov    %rax,0x20(%rbp)
  25:	mov    %rdi,%rbx
  28:	mov    0x70(%rbx),%rdx
  2c:	mov    $0x2,%edi
  31:	mov    %rdi,-0x18(%rbp)
  35:	add    $0x20,%rdx
  39:	mov    %rbp,%rdi
  3c:	add    $0xfffffffffffffff8,%rdi
  40:	mov    $0x8,%esi
  45:	callq  0xfffffffff65d906e
  4a:	mov    -0x8(%rbp),%rdx
  4e:	add    $0x40,%rdx
  52:	mov    %rbp,%rdi
  55:	add    $0xfffffffffffffff8,%rdi
  59:	mov    $0x8,%esi
  5e:	callq  0xfffffffff65d906e
  63:	mov    -0x8(%rbp),%rdi
  67:	mov    %rdi,-0x10(%rbp)
  6b:	movabs $0xffff9860ed0ba000,%r13
  75:	callq  0xfffffffff65f71ce
  7a:	mov    %rbp,%rcx
  7d:	add    $0xffffffffffffffe8,%rcx
  81:	mov    %rbx,%rdi
  84:	mov    %r13,%rsi
  87:	mov    %rax,%rdx
  8a:	mov    $0x10,%r8d
  90:	callq  0xfffffffff65d960e
  95:	xor    %eax,%eax
  97:	mov    0x0(%rbp),%rbx
  9b:	mov    0x8(%rbp),%r13
  9f:	mov    0x10(%rbp),%r14
  a3:	mov    0x18(%rbp),%r15
  a7:	add    $0x28,%rbp
  ab:	leaveq 
  ac:	retq   

 0xffffffffc03eb692爲prog->bpf_func的地址,其中的調用函數的地址是錯誤的,懷疑是工具的問題。

 以基於kprobe的ebpf爲例看下bpf_func的執行流程。
create_local_trace_kprobe首先函數調用alloc_trace_kprobe分配trace_kprobe,並初始化kprobe的pre_handler爲kprobe_dispatcher。然後調用__register_trace_kprobe註冊kprobe。
 kprobe探測函數執行的之後,執行其pre_handler也就是kprobe_dispatcher。

 static int kprobe_dispatcher(struct kprobe *kp, struct pt_regs *regs)
 {
     struct trace_kprobe *tk = container_of(kp, struct trace_kprobe, rp.kp);
     int ret = 0;
 
     raw_cpu_inc(*tk->nhit);
 
     if (tk->tp.flags & TP_FLAG_TRACE)
         kprobe_trace_func(tk, regs);
 #ifdef CONFIG_PERF_EVENTS
     if (tk->tp.flags & TP_FLAG_PROFILE)
         ret = kprobe_perf_func(tk, regs);
 #endif
     return ret;
 }

 走kprobe_perf_func分支。kprobe_perf_func函數調用trace_call_bpf()。

unsigned int trace_call_bpf(struct trace_event_call *call, void *ctx)
  {
      unsigned int ret;
  
      if (in_nmi()) /* not supported yet */
          return 1;
  
      preempt_disable();
  
      if (unlikely(__this_cpu_inc_return(bpf_prog_active) != 1)) {
          /*
           * since some bpf program is already running on this cpu,
           * don't call into another bpf program (same or different)
           * and don't send kprobe event into ring-buffer,
           * so return zero here
           */
          ret = 0;
          goto out;
       }
  
      /*
       * Instead of moving rcu_read_lock/rcu_dereference/rcu_read_unlock
       * to all call sites, we did a bpf_prog_array_valid() there to check
       * whether call->prog_array is empty or not, which is
       * a heurisitc to speed up execution.
       *
       * If bpf_prog_array_valid() fetched prog_array was
       * non-NULL, we go into trace_call_bpf() and do the actual
       * proper rcu_dereference() under RCU lock.
       * If it turns out that prog_array is NULL then, we bail out.
       * For the opposite, if the bpf_prog_array_valid() fetched pointer
       * was NULL, you'll skip the prog_array with the risk of missing
       * out of events when it was updated in between this and the
       * rcu_dereference() which is accepted risk.
       */
      ret = BPF_PROG_RUN_ARRAY_CHECK(call->prog_array, ctx, BPF_PROG_RUN);
  
   out:
      __this_cpu_dec(bpf_prog_active);
      preempt_enable();
  
      return ret;
  }

 BPF_PROG_RUN_ARRAY_CHECK宏調用prog_array中所有prog的bpf_func函數。
 看看bpf_probe_read函數,bpf_probe_read 被ebpf用於拷貝內核內存達到間接訪問的目的,

  BPF_CALL_3(bpf_probe_read, void *, dst, u32, size, const void *, unsafe_ptr)
  {
      int ret;
  
      ret = probe_kernel_read(dst, unsafe_ptr, size);
      if (unlikely(ret < 0))
          memset(dst, 0, size);
  
      return ret;
  }

 probe_kernel_read調用copy_user_enhanced_fast_string函數:

 ENTRY(copy_user_enhanced_fast_string)
     ASM_STAC
     cmpl $64,%edx
     jb .L_copy_short_string /* less then 64 bytes, avoid the costly 'rep' */
     movl %edx,%ecx
 1:  rep
     movsb
     xorl %eax,%eax
     ASM_CLAC
     ret
 
     .section .fixup,"ax"
 12: movl %ecx,%edx      /* ecx is zerorest also */
     jmp copy_user_handle_tail
     .previous
 
     _ASM_EXTABLE(1b,12b)
 ENDPROC(copy_user_enhanced_fast_string)

 三個參數,rdi是目的地址,rsi是要可拷貝的內核地址,rdx是拷貝長度。
6,7行,循環內存拷貝rsi地址的內容到rdi,直到ecx長度爲止
13行,定義一個名爲fixup的段,之後的指令加入該段,ax段屬性,可分配可執行
17行,向異常向量表添加異常處理項。1b是異常發生地址,12b是異常處理跳轉地址。
如果內核地址src非法,在do_page_fault裏,fix_exception裏查找異常向量表,將reg->ip設置12b的地址,do_page_fault執行完從異常返回後,回到12b處執行。
12b處會調用copy_user_handle_tail拷貝剩下的字節。這個函數也是同樣的機制,不在說明。

可見bpf_probe_read函數可以處理內核地址非法的情況,最後只會給用戶返回實際拷貝成功的長度。
最後,ebpf的安全性:

  • ebpf prog load的嚴格的verify機制
  • ebpf訪問內核資源需藉助各種ebpf 的helper func,helper func函數能在最壞的情況下保證安全
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章