月26日消息,努比亞官宣其2024年中興終端系列新品發(fā)布會(huì)將于2月26日在巴塞羅那的MWC2024展會(huì)上舉辦,屆時(shí)努比亞將會(huì)發(fā)布一系列產(chǎn)品,其中包括努比亞首款折疊屏手機(jī)nubia Flip。
據(jù)悉,本次發(fā)布會(huì)將要發(fā)布的產(chǎn)品包括nubia Flip 5G折疊屏手機(jī)、nubia Pad3DⅡ平板、nubia Focus 5G/Pro 5G、nubia Music、nubia Neo2 5G以及已經(jīng)在國(guó)內(nèi)發(fā)布的Z60 Ultra手機(jī)。
最受矚目的必定是努比亞首款折疊屏手機(jī)nubia Flip,采用同心圓的Deco設(shè)計(jì),外圈為攝像模組,而內(nèi)圈則是圓形的外屏。nubia Flip采用驍龍7 Gen1芯片,配備6.9英寸內(nèi)屏和1.43英寸圓形外屏。此外,該機(jī)還擁有5000萬像素主攝和1600萬像素前置相機(jī),支持懸停拍照等功能。
外觀方面,nubia Flip 5G預(yù)計(jì)將提供多種顏色選擇,機(jī)身寬度為76mm,折疊狀態(tài)下厚度為15.5mm,重214g,支持IPX2級(jí)防水和IP4X級(jí)防塵。該機(jī)還將使用4310mAh電池,支持33W快充,充滿時(shí)間約73分鐘。同時(shí),該機(jī)還支持側(cè)邊指紋識(shí)別與人臉識(shí)別、Wi-Fi 6、藍(lán)牙5.2以及兼容FeliCa的NFC功能。
此前中興已在日本推出類似造型的手機(jī)Libero Flip,預(yù)計(jì)為面向全球市場(chǎng)的改名版本。
除此之外,nubia Music的外觀同樣也是十分吸睛,手機(jī)背面采用了獨(dú)特的彩繪圖案,搭載后置雙攝,鏡頭右側(cè)似乎配備了巨大的揚(yáng)聲器。此外,在音樂播放界面,屏幕側(cè)邊還會(huì)提供燈光效果。
PConline還留意到,nubia Focus 5G/Pro 5G外觀與國(guó)內(nèi)發(fā)布的nubia Z50s/Pro相似,或同樣為面向海外市場(chǎng)的改名版本。
更多關(guān)于中興努比亞新機(jī)的詳細(xì)信息敬請(qǐng)期待在WMC2024展會(huì)的正式發(fā)布,PConline也會(huì)帶來后續(xù)的報(bào)道。
作本文時(shí),LLVM 是唯一提供 BPF 后端的編譯器套件。gcc 目前僅部分支持,但不如LLVM完善。
BPF后端在LLVM3.7版本就已經(jīng)合入了,主流的發(fā)行版在對(duì) LLVM 打包的時(shí)候就默認(rèn)啟用了 BPF 后端。因此,在大部分發(fā)行版上安 裝 clang 和 llvm 就可以將 C 代碼編譯為 BPF 對(duì)象文件了。
典型的工作流:
某些子系統(tǒng)還支持將 BPF 程序 offload 到硬件(例如網(wǎng)卡)。
查看 LLVM 支持的 BPF target:
$ llc --version
LLVM (http://llvm.org/):
LLVM version 3.8.1
Optimized build.
Default target: x86_64-unknown-linux-gnu
Host CPU: skylake
Registered Targets:
[...]
bpf - BPF (host endian)
bpfeb - BPF (big endian)
bpfel - BPF (little endian)
[...]
默認(rèn)情況下,bpf target 使用編譯時(shí)所在的 CPU 的大小端格式,即,如果 CPU 是小 端,BPF 程序就會(huì)用小端表示;如果 CPU 是大端,BPF 程序就是大端。這也和 BPF 的運(yùn) 行時(shí)行為相匹配,這樣的行為比較通用,而且大小端格式一致可以避免一些因?yàn)楦袷綄?dǎo)致的 架構(gòu)劣勢(shì)。
BPF 程序可以在大端節(jié)點(diǎn)上編譯,在小端節(jié)點(diǎn)上運(yùn)行,或者相反,因此對(duì)于交叉編譯, 引入了兩個(gè)新目標(biāo) bpfeb 和 bpfel。注意前端也需要以相應(yīng)的大小端方式運(yùn)行。
在不存在大小端混用的場(chǎng)景下,建議使用 bpf target。例如,在 x86_64 平臺(tái)上(小端 ),指定 bpf 和 bpfel 會(huì)產(chǎn)生相同的結(jié)果,因此觸發(fā)編譯的腳本不需要感知到大小端 。
下面是一個(gè)最小的完整 XDP 程序,實(shí)現(xiàn)丟棄包的功能(xdp-example.c):
#include <linux/bpf.h>
#ifndef __section
# define __section(NAME) \
__attribute__((section(NAME), used))
#endif
__section("prog")
int xdp_drop(struct xdp_md *ctx)
{
return XDP_DROP;
}
char __license[] __section("license")="GPL";
用下面的命令編譯并加載到內(nèi)核:
$ clang -O2 -Wall -target bpf -c xdp-example.c -o xdp-example.o
# ip link set dev em1 xdp obj xdp-example.o
以上命令將一個(gè) XDP 程序 attach 到一個(gè)網(wǎng)絡(luò)設(shè)備,需要是 Linux 4.11 內(nèi)核中支持 XDP 的設(shè)備,或者 4.12+ 版本的內(nèi)核。
LLVM(>=3.9) 使用正式的 BPF 機(jī)器值(machine value),即 EM_BPF(十進(jìn)制 247 ,十六進(jìn)制 0xf7),來生成對(duì)象文件。在這個(gè)例子中,程序是用 bpf target 在 x86_64 平臺(tái)上編譯的,因此下面顯示的大小端標(biāo)識(shí)是 LSB (和 MSB 相反):
$ file xdp-example.o
xdp-example.o: ELF 64-bit LSB relocatable, *unknown arch 0xf7* version 1 (SYSV), not stripped
readelf -a xdp-example.o能夠打印 ELF 文件的更詳細(xì)信息,有時(shí)在檢查生成的 section header、relocation entries 和符號(hào)表時(shí)會(huì)比較有用。
如果需要從頭開始編譯clang和LLVM內(nèi)核,可以參考下面的命令。
$ git clone https://github.com/llvm/llvm-project.git
$ cd llvm-project
$ mkdir build
$ cd build
$ cmake -DLLVM_ENABLE_PROJECTS=clang -DLLVM_TARGETS_TO_BUILD="BPF;X86" -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DLLVM_BUILD_RUNTIME=OFF -G "Unix Makefiles" ../llvm
$ make -j $(getconf _NPROCESSORS_ONLN)
$ ./bin/llc --version
LLVM (http://llvm.org/):
LLVM version x.y.zsvn
Optimized build.
Default target: x86_64-unknown-linux-gnu
Host CPU: skylake
Registered Targets:
bpf - BPF (host endian)
bpfeb - BPF (big endian)
bpfel - BPF (little endian)
x86 - 32-bit X86: Pentium-Pro and above
x86-64 - 64-bit X86: EM64T and AMD64
$ export PATH=$PWD/bin:$PATH # add to ~/.bashrc
確保--version中提到了Optimized build,否則當(dāng)LLVM處于調(diào)試模式時(shí),程序的編譯時(shí)間將顯著增加(增加10倍甚至更多)。
若是要 debug,clang 可以生成下面這樣的匯編器輸出:
$ clang -O2 -S -Wall -target bpf -c xdp-example.c -o xdp-example.S
$ cat xdp-example.S
.text
.section prog,"ax",@progbits
.globl xdp_drop
.p2align 3
xdp_drop: # @xdp_drop
# BB#0:
r0=1
exit
.section license,"aw",@progbits
.globl __license # @__license
__license:
.asciz "GPL"
LLVM 從 6.0 開始,還包括了匯編解析器(assembler parser)的支持。可以直接使用 BPF 匯編指令編程,然后使用 llvm-mc 將其匯編成一個(gè)目標(biāo)文件。 例如,可以將前面的 xdp-example.S 重新變回對(duì)象文件:
$ llvm-mc -triple bpf -filetype=obj -o xdp-example.o xdp-example.S
較新版本(>=4.0)的 LLVM 編譯時(shí)加上 -g將調(diào)試信息以 dwarf 格式存儲(chǔ)到對(duì)象文件中。
$ clang -O2 -g -Wall -target bpf -c xdp-example.c -o xdp-example.o
$ llvm-objdump -S --no-show-raw-insn xdp-example.o
xdp-example.o: file format ELF64-BPF
Disassembly of section prog:
xdp_drop:
; {
0: r0=1
; return XDP_DROP;
1: exit
llvm-objdump 工具能夠用編譯的 C 源碼對(duì)匯編輸出添加注解。這里 的例子過于簡(jiǎn)單,沒有幾行 C 代碼;但注意上面的 0 和 1 行號(hào),這些行號(hào)直接對(duì)應(yīng)到內(nèi)核的校驗(yàn)器日志(見下面的輸出)。這意味著假如 BPF 程序被校驗(yàn)器拒絕了, llvm-objdump能幫助你將 BPF 指令關(guān)聯(lián)到原始的 C 代碼,對(duì)于分析來說非常有用。
# ip link set dev em1 xdp obj xdp-example.o verb
Prog section 'prog' loaded (5)!
- Type: 6
- Instructions: 2 (0 over limit)
- License: GPL
Verifier analysis:
0: (b7) r0=1
1: (95) exit
processed 2 insns
從上面的校驗(yàn)器分析可以看出,llvm-objdump 的輸出和內(nèi)核中的 BPF 匯編是相同的。
去掉 -no-show-raw-insn 選項(xiàng)還可以以十六進(jìn)制格式在每行匯編代碼前面打印原始的 struct bpf_insn:
$ llvm-objdump -S xdp-example.o
xdp-example.o: file format ELF64-BPF
Disassembly of section prog:
xdp_drop:
; {
0: b7 00 00 00 01 00 00 00 r0=1
; return foo();
1: 95 00 00 00 00 00 00 00 exit
對(duì)于 LLVM IR 調(diào)試,BPF 的編譯過程可以分為兩個(gè)步驟:首先生成一個(gè)二進(jìn)制 LLVM IR 臨 時(shí)文件 xdp-example.bc,然后將其傳遞給 llc:
$ clang -O2 -Wall -target bpf -emit-llvm -c xdp-example.c -o xdp-example.bc $ llc xdp-example.bc -march=bpf -filetype=obj -o xdp-example.o
生成的 LLVM IR 還可以 dump 成可讀的格式:
$ clang -O2 -Wall -emit-llvm -S -c xdp-example.c -o -
LLVM 能將調(diào)試信息(例如對(duì)程序使用的數(shù)據(jù)的描述)attach 到 BPF 對(duì)象文件。默認(rèn)情況下使用 DWARF 格式。
BPF 使用了一個(gè)高度簡(jiǎn)化的版本,稱為 BTF (BPF Type Format)。生成的 DWARF 可以轉(zhuǎn)換成 BTF 格式,然后通過 BPF 對(duì)象加載器加載到內(nèi)核。內(nèi)核驗(yàn)證 BTF 數(shù)據(jù)的正確性, 并跟蹤 BTF 數(shù)據(jù)中包含的數(shù)據(jù)類型。
這樣的話,就可以用鍵和值對(duì) BPF map 打一些注解存儲(chǔ)到 BTF 數(shù)據(jù)中,這 樣下次 dump map 時(shí),除了 map 內(nèi)的數(shù)據(jù)外還會(huì)打印出相關(guān)的類型信息。這對(duì)內(nèi)省( introspection)、調(diào)試和格式化打印都很有幫助。注意,BTF 是一種通用的調(diào)試數(shù)據(jù)格式,因此任何從 DWARF 轉(zhuǎn)換成的 BTF 數(shù)據(jù)都可以被加載(例如,內(nèi)核 vmlinux DWARF 數(shù) 據(jù)可以轉(zhuǎn)換成 BTF 然后加載)。后者對(duì)于未來 BPF 的跟蹤尤其有用。
將 DWARF 格式的調(diào)試信息轉(zhuǎn)換成 BTF 格式需要用到 elfutils (>=0.173) 工具。 如果沒有這個(gè)工具,那需要在 llc 編譯時(shí)打開 -mattr=dwarfris 選項(xiàng):
$ llc -march=bpf -mattr=help |& grep dwarfris
dwarfris - Disable MCAsmInfo DwarfUsesRelocationsAcrossSections.
[...]
使用 -mattr=dwarfris 是因?yàn)?dwarfris (dwarf relocation in section) 選項(xiàng)禁用了 DWARF 和 ELF 的符號(hào)表之間的 DWARF cross-section 重定位,因?yàn)?libdw 不支持 BPF 重定位。不打開這個(gè)選項(xiàng)的話,pahole 這類工具將無法正確地從對(duì)象中 dump 結(jié)構(gòu)。
elfutils (>=0.173) 實(shí)現(xiàn)了合適的 BPF 重定位,因此沒有打開 -mattr=dwarfris 選項(xiàng)也能正常工作。它可以從對(duì)象文件中的 DWARF 或 BTF 信息 dump 結(jié)構(gòu)。目前 pahole 使用 LLVM 生成的 DWARF 信息,但未來它可能會(huì)使用 BTF 信息。
將 DWARF 轉(zhuǎn)換成 BTF 格式需要使用較新的 pahole 版本(>=1.12),然后指定 -J 選項(xiàng)。 檢查所用的 pahole 版本是否支持 BTF(注意,pahole 會(huì)用到 llvm-objcopy,因此 也要檢查后者是否已安裝):
$ pahole --help | grep BTF -J, --btf_encode Encode as BTF
生成調(diào)試信息還需要前端的支持,在 clang 編譯時(shí)指定 -g 選項(xiàng),生成源碼級(jí)別的調(diào)試信息。注意,不管 llc 是否指定了 dwarfris 選項(xiàng),-g 都是需要指定的。生成目標(biāo)文件的完整示例:
$ clang -O2 -g -Wall -target bpf -emit-llvm -c xdp-example.c -o xdp-example.bc
$ llc xdp-example.bc -march=bpf -mattr=dwarfris -filetype=obj -o xdp-example.o
或者,只使用 clang 這一個(gè)工具來編譯帶調(diào)試信息的 BPF 程序(同樣,如果有合適的 elfutils 版本,dwarfris 選項(xiàng)可以省略):
$ clang -target bpf -O2 -g -c -Xclang -target-feature -Xclang +dwarfris -c xdp-example.c -o xdp-example.o
成功編譯后,可以使用pahole根據(jù)DWARF信息正確 dump BPF 程序的數(shù)據(jù)結(jié)構(gòu):
$ pahole xdp-example.o
struct xdp_md {
__u32 data; /* 0 4 */
__u32 data_end; /* 4 4 */
__u32 data_meta; /* 8 4 */
/* size: 12, cachelines: 1, members: 3 */
/* last cacheline: 12 bytes */
};
通過選項(xiàng)-J選項(xiàng) pahole最終可以從DWARF生成BTF。在對(duì)象文件中,DWARF 數(shù)據(jù)將仍然伴隨著新加入的 BTF 數(shù)據(jù)一起保留。完整的 clang 和 pahole 示例:
$ clang -target bpf -O2 -Wall -g -c -Xclang -target-feature -Xclang +dwarfris -c xdp-example.c -o xdp-example.o
$ pahole -J xdp-example.o
通過 readelf 工具可以看到多了一個(gè) .BTF段。
$ readelf -a xdp-example.o
[...]
[18] .BTF PROGBITS 0000000000000000 00000671
[...]
BPF 加載器(例如 iproute2)會(huì)檢測(cè)和加載 BTF section,因此給 BPF map 注釋( annotate)類型信息。
LLVM 默認(rèn)用 BPF 基礎(chǔ)指令集生成代碼, 以確保生成的對(duì)象文件也能被稍老的 LTS 內(nèi)核(例如 4.9+)加載。 但 LLVM 提供了一個(gè) BPF 后端選項(xiàng) -mcpu,用來指定特定的 BPF 指令集版本, 即 BPF 基礎(chǔ)指令集之上的指令集擴(kuò)展(instruction set extensions),以生成更高效和 體積更小的代碼。
下面代碼可以查詢當(dāng)前支持的-mcpu 類型:
$ llc -march bpf -mcpu=help
Available CPUs for this target:
generic - Select the generic processor.
probe - Select the probe processor.
v1 - Select the v1 processor.
v2 - Select the v2 processor.
[...]
推薦使用 -mcpu=probe ,這也是 Cilium 內(nèi)部在使用的類型。使用這種類型時(shí), LLVM BPF 后端會(huì)向內(nèi)核詢問可用的 BPF 指令集擴(kuò)展,如果找到可用的,就會(huì)使用相應(yīng)的指令集來編譯 BPF 程序。
使用 llc 和 -mcpu=probe 的完整示例:
$ clang -O2 -Wall -target bpf -emit-llvm -c xdp-example.c -o xdp-example.bc
$ llc xdp-example.bc -march=bpf -mcpu=probe -filetype=obj -o xdp-example.o
通常來說,LLVM IR 生成是架構(gòu)無關(guān)的。但使用 clang 編譯時(shí)是否指定 -target bpf 是有小區(qū)別的,取決于不同的平臺(tái)架構(gòu)(x86_64、arm64 或其他),-target 的 默認(rèn)配置可能不同。
引用內(nèi)核文檔 Documentation/bpf/bpf_devel_QA.txt:
native target 主要用于跟蹤內(nèi)核中的 struct pt_regs,這個(gè)結(jié)構(gòu)體對(duì) CPU 寄存器進(jìn)行映射,或者是跟蹤其他一些能感知 CPU 寄存器位寬的內(nèi)核結(jié)構(gòu)體。除此之外的其他場(chǎng)景,例如網(wǎng)絡(luò)場(chǎng)景,都建議使用 clang -target bpf。
另外,LLVM 從 7.0 開始支持 32 位子寄存器和 BPF ALU32 指令。另外,新加入了一個(gè)代碼生成屬性 alu32。當(dāng)指定這個(gè)參數(shù)時(shí),LLVM 會(huì)嘗試盡可能地使用 32 位子寄存器,例如當(dāng)涉及到 32 位操作時(shí)。32 位子寄存器及相應(yīng)的 ALU 指令組成了 ALU32 指令。例如, 對(duì)于下面的示例代碼:
$ cat 32-bit-example.c
void cal(unsigned int *a, unsigned int *b, unsigned int *c)
{
unsigned int sum=*a + *b;
*c=sum;
}
使用默認(rèn)的代碼生成選項(xiàng),產(chǎn)生的匯編代碼如下:
$ clang -target bpf -emit-llvm -S 32-bit-example.c
$ llc -march=bpf 32-bit-example.ll
$ cat 32-bit-example.s
cal:
r1=*(u32 *)(r1 + 0)
r2=*(u32 *)(r2 + 0)
r2 +=r1
*(u32 *)(r3 + 0)=r2
exit
可以看到默認(rèn)使用的是 r 系列寄存器,這些都是 64 位寄存器,這意味著其中的加法都是 64 位加法。現(xiàn)在,如果指定 -mattr=+alu32 強(qiáng)制要求使用 32 位,生成的匯編代碼 如下:
$ llc -march=bpf -mattr=+alu32 32-bit-example.ll
$ cat 32-bit-example.s
cal:
w1=*(u32 *)(r1 + 0)
w2=*(u32 *)(r2 + 0)
w2 +=w1
*(u32 *)(r3 + 0)=w2
exit
可以看到這次使用的是 w 系列寄存器,這些是 32 位子寄存器。
最終生成的代碼中使用 32 位子寄存器可能會(huì)減小類型擴(kuò)展指令的數(shù)量。另外,它對(duì) 32 位架構(gòu)的內(nèi)核 eBPF JIT 編譯器也有所幫助,因?yàn)?原來這些編譯器都是用 32 位模擬 64 位 eBPF 寄存器,其中使用了很多 32 位指令來操作 高 32 bit。即使寫 32 位子寄存器的操作仍然需要對(duì)高 32 位清零,但只要確保從 32 位 子寄存器的讀操作只會(huì)讀取低 32 位,那只要 JIT 編譯器已經(jīng)知道某個(gè)寄存器的定義只有 子寄存器讀操作,那對(duì)高 32 位的操作指令就可以避免。
用 C 語言編寫 BPF 程序不同于用 C 語言做應(yīng)用開發(fā),有一些陷阱需要注意。本節(jié)列出了 二者的一些不同之處。
BPF 不支持共享庫(Shared libraries)。但是,可以將常規(guī)的庫代碼(library code)放到頭文件中,然后在主程序中 include 這些頭文件,例如 Cilium 就大量使用了這種方式 (可以查看 bpf/lib/ 文件夾)。另外,也可以 include 其他的一些頭文件,例如內(nèi)核 或其他庫中的頭文件,復(fù)用其中的靜態(tài)內(nèi)聯(lián)函數(shù)(static inline functions)或宏/定義( macros / definitions)。
內(nèi)核 4.16+ 和 LLVM 6.0+ 之后已經(jīng)支持 BPF-to-BPF 函數(shù)調(diào)用。對(duì)于任意給定的程序片段 ,在此之前的版本只能將全部代碼編譯和內(nèi)聯(lián)成一個(gè)順序的 BPF 指令序列。在這種情況下,最佳實(shí)踐就是為每個(gè)庫函數(shù)都使用一個(gè)像 __inline 一樣的注解,下面的例子中會(huì)看到。推薦使用 always_inline,因?yàn)榫幾g器可能會(huì)對(duì)只注解為 inline 的長(zhǎng)函數(shù)仍然做 uninline 操 作。
如果是后者,LLVM 會(huì)在 ELF 文件中生成一個(gè)重定位項(xiàng)(relocation entry),BPF ELF 加載器(例如 iproute2)無法解析這個(gè)重定位項(xiàng),因此會(huì)產(chǎn)生一條錯(cuò)誤,因?yàn)閷?duì)加載器來說只有 BPF maps 是合法的、能夠處理的重定位項(xiàng)。
#include <linux/bpf.h>
#ifndef __section
# define __section(NAME) \
__attribute__((section(NAME), used))
#endif
#ifndef __inline
# define __inline \
inline __attribute__((always_inline))
#endif
static __inline int foo(void)
{
return XDP_DROP;
}
__section("prog")
int xdp_drop(struct xdp_md *ctx)
{
return foo();
}
char __license[] __section("license")="GPL";
BPF C 程序大量使用 section annotations。一個(gè) C 文件典型情況下會(huì)分為 3 個(gè)或更多個(gè) section。BPF ELF 加載器利用這些名字來提取和準(zhǔn)備相關(guān)的信息,以通過 bpf()系統(tǒng)調(diào)用加載程序和 maps。例如,查找創(chuàng)建 map 所需的元數(shù)據(jù)和 BPF 程序的 license 信息時(shí),iproute2 會(huì)分別使用 maps 和 license 作為默認(rèn)的 section 名字。注意在程序創(chuàng)建時(shí) license section 也會(huì)加載到內(nèi)核,如果程序使用的是兼容 GPL 的協(xié)議,這些信息就可以啟用那些 GPL-only 的輔助函數(shù),例如 bpf_ktime_get_ns() 和 bpf_probe_read() 。
其余的 section 名字都是和特定的 BPF 程序代碼相關(guān)的,例如,下面經(jīng)過修改之后的代碼包含兩個(gè)程序 section:ingress 和 egress。這個(gè)非常簡(jiǎn)單的示例展示了不同 section之間可以共享 BPF map 和常規(guī)的靜態(tài)內(nèi)聯(lián)輔助函數(shù)( 例如 account_data())。
這里將原來的 xdp-example.c 修改為 tc-example.c,然后用 tc 命令加載,attach 到 一個(gè) netdevice 的 ingress 或 egress hook。該程序?qū)鬏數(shù)淖止?jié)進(jìn)行計(jì)數(shù),存儲(chǔ)在一 個(gè)名為 acc_map 的 BPF map 中,這個(gè) map 有兩個(gè)槽,分別用于 ingress hook 和 egress hook 的流量統(tǒng)計(jì)。
#include <linux/bpf.h>
#include <linux/pkt_cls.h>
#include <stdint.h>
#include <iproute2/bpf_elf.h>
#ifndef __section
# define __section(NAME) \
__attribute__((section(NAME), used))
#endif
#ifndef __inline
# define __inline \
inline __attribute__((always_inline))
#endif
#ifndef lock_xadd
# define lock_xadd(ptr, val) \
((void)__sync_fetch_and_add(ptr, val))
#endif
#ifndef BPF_FUNC
# define BPF_FUNC(NAME, ...) \
(*NAME)(__VA_ARGS__)=(void *)BPF_FUNC_##NAME
#endif
static void *BPF_FUNC(map_lookup_elem, void *map, const void *key);
struct bpf_elf_map acc_map __section("maps")={
.type =BPF_MAP_TYPE_ARRAY,
.size_key=sizeof(uint32_t),
.size_value=sizeof(uint32_t),
.pinning=PIN_GLOBAL_NS,
.max_elem=2,
};
static __inline int account_data(struct __sk_buff *skb, uint32_t dir)
{
uint32_t *bytes;
bytes=map_lookup_elem(&acc_map, &dir);
if (bytes)
lock_xadd(bytes, skb->len);
return TC_ACT_OK;
}
__section("ingress")
int tc_ingress(struct __sk_buff *skb)
{
return account_data(skb, 0);
}
__section("egress")
int tc_egress(struct __sk_buff *skb)
{
return account_data(skb, 1);
}
char __license[] __section("license")="GPL";
這個(gè)例子還展示了其他一些很有用的東西,在開發(fā)過程中要注意。
首先,include 了內(nèi)核頭文件、標(biāo)準(zhǔn) C 頭文件和一個(gè)特定的 iproute2 頭文件 iproute2/bpf_elf.h,后者定義了struct bpf_elf_map。iproute2 有一個(gè)通用的 BPF ELF 加載器,因此 struct bpf_elf_map的定義對(duì)于 XDP 和 tc 類型的程序是完全一樣的 。
其次,程序中每條 struct bpf_elf_map 記錄定義一個(gè) map,這個(gè)記錄包含了生成一個(gè)(ingress 和 egress 程序需要用到的)map 所需的全部信息(例如 key/value 大小)。這個(gè)結(jié)構(gòu)體的定義必須放在 maps section,這樣加載器才能找到它。可以用這個(gè)結(jié)構(gòu)體聲明很多名字不同的變量,但這些聲明前面必須加上 __section("maps") 注解。
結(jié)構(gòu)體 struct bpf_elf_map 是特定于 iproute2 的。不同的 BPF ELF 加載器有不同的格式,例如,內(nèi)核源碼樹中的 libbpf(主要是 perf 在用)就有一個(gè)不同的規(guī)范 (結(jié)構(gòu)體定義)。iproute2 保證 struct bpf_elf_map 的后向兼容性。Cilium 采用的 是 iproute2 模型。
另外,這個(gè)例子還展示了 BPF 輔助函數(shù)是如何映射到 C 代碼以及如何被使用的。這里首先定義了一個(gè)宏 BPF_FUNC,接受一個(gè)函數(shù)名 NAME 以及其他的任意參數(shù)。然后用這個(gè)宏聲明了一 個(gè) NAME 為 map_lookup_elem 的函數(shù),經(jīng)過宏展開后會(huì)變成 BPF_FUNC_map_lookup_elem 枚舉值,后者以輔助函數(shù)的形式定義在 uapi/linux/bpf.h 。當(dāng)隨后這個(gè)程序被加載到內(nèi)核時(shí),校驗(yàn)器會(huì)檢查傳入的參數(shù)是否是期望的類型,如果是, 就將輔助函數(shù)調(diào)用重新指向某個(gè)真正的函數(shù)調(diào)用。另外, map_lookup_elem() 還展示了 map 是如何傳遞給 BPF 輔助函數(shù)的。這里,maps section 中的 &acc_map 作為第一個(gè)參數(shù)傳遞給 map_lookup_elem()。
由于程序中定義的數(shù)組 map (array map)是全局的,因此計(jì)數(shù)時(shí)需要使用原子操作,這里是使用了 lock_xadd()。LLVM 將 __sync_fetch_and_add() 作為一個(gè)內(nèi)置函數(shù)映射到 BPF 原子加指令,即 BPF_STX | BPF_XADD | BPF_W(根據(jù)字長(zhǎng)實(shí)際選擇指令)。
另外,struct bpf_elf_map 中的 .pinning 字段初始化為 PIN_GLOBAL_NS,這意味 著 tc 會(huì)將這個(gè) map 作為一個(gè)節(jié)點(diǎn)(node)釘(pin)到 BPF 偽文件系統(tǒng)。默認(rèn)情況下, 這個(gè)變量 acc_map 將被釘?shù)?/sys/fs/bpf/tc/globals/acc_map。
因此,在加載 ingress 程序時(shí),tc 會(huì)先查找這個(gè) map 在 BPF 文件系統(tǒng)中是否存在,不存在就創(chuàng)建一個(gè)。創(chuàng)建成功后,map 會(huì)被釘?shù)?BPF 文件系統(tǒng),因此當(dāng) egress 程序通過 tc 加載之后,它就會(huì)發(fā)現(xiàn)這個(gè) map 存在了,接下來會(huì)復(fù)用這個(gè) map 而不是再創(chuàng)建一個(gè)新的。在 map 存在的情況下,加載器還會(huì)確保 map 的屬性是匹配的, 例如 key/value 大小等等。
就像 tc 可以從同一 map 獲取數(shù)據(jù)一樣,第三方應(yīng)用也可以用 bpf 系統(tǒng)調(diào)用中的 BPF_OBJ_GET 命令創(chuàng)建一個(gè)指向某個(gè) map 實(shí)例的新文件描述符,然后用這個(gè)描述符來查看/更新/刪除 map 中的數(shù)據(jù)。
上面的代碼可以通過 clang 編譯和 iproute2 加載:
$ clang -O2 -Wall -target bpf -c tc-example.c -o tc-example.o
# tc qdisc add dev em1 clsact
# tc filter add dev em1 ingress bpf da obj tc-example.o sec ingress
# tc filter add dev em1 egress bpf da obj tc-example.o sec egress
# tc filter show dev em1 ingress
filter protocol all pref 49152 bpf
filter protocol all pref 49152 bpf handle 0x1 tc-example.o:[ingress] direct-action id 1 tag c5f7825e5dac396f
# tc filter show dev em1 egress
filter protocol all pref 49152 bpf
filter protocol all pref 49152 bpf handle 0x1 tc-example.o:[egress] direct-action id 2 tag b2fd5adc0f262714
# mount | grep bpf
sysfs on /sys/fs/bpf type sysfs (rw,nosuid,nodev,noexec,relatime,seclabel)
bpf on /sys/fs/bpf type bpf (rw,relatime,mode=0700)
# tree /sys/fs/bpf/
/sys/fs/bpf/
+-- ip -> /sys/fs/bpf/tc/
+-- tc
| +-- globals
| +-- acc_map
+-- xdp -> /sys/fs/bpf/tc/
4 directories, 1 file
以上步驟指向完成后,當(dāng)包經(jīng)過 em 設(shè)備時(shí),BPF map 中的計(jì)數(shù)器就會(huì)遞增。
出于第 1 條中提到的原因(只支持 BPF maps 重定位),BPF 不能使用全局變量 ,而常規(guī) C 程序中是可以的。
但是,我們有間接的方式實(shí)現(xiàn)全局變量的效果:BPF 程序可以使用一個(gè) BPF_MAP_TYPE_PERCPU_ARRAY 類型的、只有一個(gè)槽(slot)的、可以存放任意類型數(shù)據(jù)的 BPF map。這可以實(shí)現(xiàn)全局變量的效果,BPF 程序在執(zhí)行期間不會(huì)被內(nèi)核搶占,因此可以用單個(gè) map entry 作為一個(gè) scratch buffer 使用,存儲(chǔ)臨時(shí)數(shù)據(jù)。例如擴(kuò)展 BPF 棧的限制(512 字節(jié))。這種方式在尾調(diào)用中也是可以工作的,因?yàn)槲舱{(diào)用執(zhí)行期間也不會(huì)被搶占。
另外,如果要在不同次 BPF 程序執(zhí)行之間保持狀態(tài),使用常規(guī)的 BPF map 就可以了。
BPF C 程序中不允許定義 const 字符串或其他數(shù)組,原因和第 1 點(diǎn)及第 3 點(diǎn)一樣,即 ,ELF 文件中生成的重定位項(xiàng)(relocation entries)會(huì)被加載器拒絕,因?yàn)椴环霞虞d器的 ABI(加載器也無法修復(fù)這些重定位項(xiàng),因?yàn)檫@需要對(duì)已經(jīng)編譯好的 BPF 序列進(jìn)行大范圍的重寫)。
將來 LLVM 可能會(huì)檢測(cè)這種情況,提前將錯(cuò)誤拋給用戶。現(xiàn)在可以用下面的trace_printk()輔助函數(shù)來作為短期解決方式:
static void BPF_FUNC(trace_printk, const char *fmt, int fmt_size, ...);
#ifndef printk
# define printk(fmt, ...) \
({ \
char ____fmt[]=fmt; \
trace_printk(____fmt, sizeof(____fmt), ##__VA_ARGS__); \
})
#endif
有了上面的定義,程序就可以自然地使用這個(gè)宏,例如 printk("skb len:%u\n", skb->len);。 輸出會(huì)寫到 trace pipe,用 tc exec bpf dbg 命令可以獲取這些打印的消息。
不過,使用 trace_printk() 輔助函數(shù)也有一些不足,因此不建議在生產(chǎn)環(huán)境使用。每次調(diào)用這個(gè)輔助函數(shù)時(shí),常量字符串(例如 "skb len:%u\n")都需要加載到 BPF 棧,但這個(gè)輔助函數(shù)最多只能接受 5 個(gè)參數(shù),因此使用這個(gè)函數(shù)輸出信息時(shí)只能傳遞三個(gè)參數(shù)。
因此,雖然這個(gè)輔助函數(shù)對(duì)快速調(diào)試很有用,但(對(duì)于網(wǎng)絡(luò)程序)還是推薦使用 skb_event_output() 或 xdp_event_output() 輔助函數(shù)。這兩個(gè)函數(shù)接受從 BPF 程序傳遞自定義的結(jié)構(gòu)體類型參數(shù),然后將參數(shù)以及可選的包數(shù)據(jù)(packet sample)放到 perf event ring buffer。例如,Cilium monitor 利用這些輔助函數(shù)實(shí)現(xiàn)了一個(gè)調(diào)試框架,以及在發(fā)現(xiàn)違反網(wǎng)絡(luò)策略時(shí)發(fā)出通知等功能。這些函數(shù)通過一個(gè)無鎖的、內(nèi)存映射的、 per-CPU 的 perf ring buffer 傳遞數(shù)據(jù),因此要遠(yuǎn)快于 trace_printk()。
因?yàn)?BPF 程序除了調(diào)用 BPF 輔助函數(shù)之外無法執(zhí)行任何函數(shù)調(diào)用,因此常規(guī)的庫代碼必須實(shí)現(xiàn)為內(nèi)聯(lián)函數(shù)。另外,LLVM 也提供了一些可以用于特定大小(這里是 n)的內(nèi)置函數(shù) ,這些函數(shù)永遠(yuǎn)都會(huì)被內(nèi)聯(lián):
#ifndef memset
# define memset(dest, chr, n) __builtin_memset((dest), (chr), (n))
#endif
#ifndef memcpy
# define memcpy(dest, src, n) __builtin_memcpy((dest), (src), (n))
#endif
#ifndef memmove
# define memmove(dest, src, n) __builtin_memmove((dest), (src), (n))
#endif
LLVM 后端中的某個(gè)問題會(huì)導(dǎo)致內(nèi)置的 memcmp() 有某些邊界場(chǎng)景下無法內(nèi)聯(lián),因此在這個(gè)問題解決之前不推薦使用這個(gè)函數(shù)。
內(nèi)核中的 BPF 校驗(yàn)器除了對(duì)其他的控制流進(jìn)行圖驗(yàn)證之外,還會(huì)對(duì)所有程序路徑執(zhí)行深度優(yōu)先搜索,確保其中不存在循環(huán)。這樣做的目的是確保程序永遠(yuǎn)會(huì)結(jié)束。
但可以使用 #pragma unroll 指令實(shí)現(xiàn)常量的、不超過一定上限的循環(huán)。下面是一個(gè)例子 :
#pragma unroll
for (i=0; i < IPV6_MAX_HEADERS; i++) {
switch (nh) {
case NEXTHDR_NONE:
return DROP_INVALID_EXTHDR;
case NEXTHDR_FRAGMENT:
return DROP_FRAG_NOSUPPORT;
case NEXTHDR_HOP:
case NEXTHDR_ROUTING:
case NEXTHDR_AUTH:
case NEXTHDR_DEST:
if (skb_load_bytes(skb, l3_off + len, &opthdr, sizeof(opthdr)) < 0)
return DROP_INVALID;
nh=opthdr.nexthdr;
if (nh==NEXTHDR_AUTH)
len +=ipv6_authlen(&opthdr);
else
len +=ipv6_optlen(&opthdr);
break;
default:
*nexthdr=nh;
return len;
}
}
另外一種實(shí)現(xiàn)循環(huán)的方式是:用一個(gè) BPF_MAP_TYPE_PERCPU_ARRAY map 作為本地 scratch space(存儲(chǔ)空間),然后用尾調(diào)用的方式調(diào)用函數(shù)自身。雖然這種方式更加動(dòng)態(tài),但目前 最大只支持 34 層(原始程序,外加 33 次尾調(diào)用)嵌套調(diào)用。
將來 BPF 可能會(huì)提供一些更加原生、但有一定限制的循環(huán)。
尾調(diào)用能夠從一個(gè)程序調(diào)到另一個(gè)程序,提供了在運(yùn)行時(shí)(runtime)原子地改變程序行為的靈活性。為了選擇要跳轉(zhuǎn)到哪個(gè)程序,尾調(diào)用使用了 程序數(shù)組 map( BPF_MAP_TYPE_PROG_ARRAY),將 map 及其索引(index)傳遞給將要跳轉(zhuǎn)到的程序。跳轉(zhuǎn)動(dòng)作一旦完成,就沒有辦法返回到原來的程序;但如果給定的 map 索引中沒有程序(無法跳轉(zhuǎn)),執(zhí)行會(huì)繼續(xù)在原來的程序中執(zhí)行。
例如,可以用尾調(diào)用實(shí)現(xiàn)解析器的不同階段,可以在運(yùn)行時(shí)(runtime)更新這些階段的新解析特性。
尾調(diào)用的另一個(gè)用處是事件通知,例如,Cilium 可以在運(yùn)行時(shí)(runtime)開啟或關(guān)閉丟棄包的通知,其中對(duì) skb_event_output() 的調(diào)用就是發(fā)生在被尾調(diào)用的程序中。因此,在常規(guī)情況下,執(zhí)行的永遠(yuǎn)是從上到下的路徑( fall-through path),當(dāng)某個(gè)程序被加入到相關(guān)的 map 索引之后,程序就會(huì)解析元數(shù)據(jù), 觸發(fā)向用戶空間守護(hù)進(jìn)程發(fā)送事件通知。
程序數(shù)組 map 非常靈活, map 中每個(gè)索引對(duì)應(yīng)的程序可以實(shí)現(xiàn)各自的動(dòng)作。 例如,attach 到 tc 或 XDP 的 root 程序執(zhí)行初始的、跳轉(zhuǎn)到程序數(shù)組 map 中索引為 0 的程序,然后執(zhí)行流量抽樣(traffic sampling),然后跳轉(zhuǎn)到索引為 1 的程序,在那個(gè)程序中應(yīng)用防火墻策略,然后就可以決定是丟地包還是將其送到索引為 2 的程序中繼續(xù)處理,在后者中,可能可能會(huì)被 mangle 然后再次通過某個(gè)接口發(fā)送出去。在程序數(shù)據(jù) map 之中是可以隨意跳轉(zhuǎn)的。當(dāng)達(dá)到尾調(diào)用的最大調(diào)用深度時(shí),內(nèi)核最終會(huì)執(zhí)行 fall-through path。
一個(gè)使用尾調(diào)用的最小程序示例:
[...]
#ifndef __stringify
# define __stringify(X) #X
#endif
#ifndef __section
# define __section(NAME) \
__attribute__((section(NAME), used))
#endif
#ifndef __section_tail
# define __section_tail(ID, KEY) \
__section(__stringify(ID) "/" __stringify(KEY))
#endif
#ifndef BPF_FUNC
# define BPF_FUNC(NAME, ...) \
(*NAME)(__VA_ARGS__)=(void *)BPF_FUNC_##NAME
#endif
#define BPF_JMP_MAP_ID 1
static void BPF_FUNC(tail_call, struct __sk_buff *skb, void *map,
uint32_t index);
struct bpf_elf_map jmp_map __section("maps")={
.type =BPF_MAP_TYPE_PROG_ARRAY,
.id =BPF_JMP_MAP_ID,
.size_key=sizeof(uint32_t),
.size_value=sizeof(uint32_t),
.pinning=PIN_GLOBAL_NS,
.max_elem=1,
};
__section_tail(BPF_JMP_MAP_ID, 0)
int looper(struct __sk_buff *skb)
{
printk("skb cb: %u\n", skb->cb[0]++);
tail_call(skb, &jmp_map, 0);
return TC_ACT_OK;
}
__section("prog")
int entry(struct __sk_buff *skb)
{
skb->cb[0]=0;
tail_call(skb, &jmp_map, 0);
return TC_ACT_OK;
}
char __license[] __section("license")="GPL";
加載這個(gè)示例程序時(shí),tc 會(huì)創(chuàng)建其中的程序數(shù)組(jmp_map 變量),并將其釘?shù)?BPF 文件系統(tǒng)中全局命名空間下名為的 jump_map 位置。而且,iproute2 中的 BPF ELF 加載器也會(huì)識(shí)別出標(biāo)記為 __section_tail() 的 section。 jmp_map 的 id 字段會(huì) 跟__section_tail() 中的 id 字段(這里初始化為常量 JMP_MAP_ID)做匹配,因此程序能加載到用戶指定的索引,在上面的例子中這個(gè)索引是 0。然后,所有的尾調(diào)用 section 將會(huì)被 iproute2 加載器處理,關(guān)聯(lián)到 map 中。這個(gè)機(jī)制并不是 tc 特有的, iproute2 支持的其他 BPF 程序類型(例如 XDP、lwt)也適用。
生成的 elf 包含 section headers,描述 map id 和 map 內(nèi)的條目:
$ llvm-objdump -S --no-show-raw-insn prog_array.o | less
prog_array.o: file format ELF64-BPF
Disassembly of section 1/0:
looper:
0: r6=r1
1: r2=*(u32 *)(r6 + 48)
2: r1=r2
3: r1 +=1
4: *(u32 *)(r6 + 48)=r1
5: r1=0 ll
7: call -1
8: r1=r6
9: r2=0 ll
11: r3=0
12: call 12
13: r0=0
14: exit
Disassembly of section prog:
entry:
0: r2=0
1: *(u32 *)(r1 + 48)=r2
2: r2=0 ll
4: r3=0
5: call 12
6: r0=0
7: exi
在這個(gè)例子中,section 1/0 表示 looper() 函數(shù)位于 map 1 中,在 map 1 內(nèi)的位置是 0。
被釘住 map 可以被用戶空間應(yīng)用(例如 Cilium daemon)讀取,也可以被 tc 本身讀取,因?yàn)?tc 可能會(huì)用新的程序替換原來的程序,此時(shí)可能需要讀取 map 內(nèi)容。 更新是原子的。
tc 執(zhí)行尾調(diào)用 map 更新(tail call map updates)的例子:
$ tc exec bpf graft m:globals/jmp_map key 0 obj new.o sec foo
如果 iproute2 需要更新被釘住的程序數(shù)組,可以使用 graft 命令。上面的例子中指向的是 globals/jmp_map,那 tc 將會(huì)用一個(gè)新程序更新位于 index/key 為 0 的 map, 這個(gè)新程序位于對(duì)象文件 new.o 中的 foo section。
BPF 程序的最大棧空間是 512 字節(jié),在使用 C 語言實(shí)現(xiàn) BPF 程序時(shí)需要考慮到這一點(diǎn)。 但正如在第 3 點(diǎn)中提到的,可以通過一個(gè)只有一條記錄的 BPF_MAP_TYPE_PERCPU_ARRAY map 來繞過這限制,增大 scratch buffer 空間。
LLVM 6.0 以后支持 BPF 內(nèi)聯(lián)匯編,在某些場(chǎng)景下可能會(huì)用到。下面這個(gè)玩具示例程序( 沒有實(shí)際意義)展示了一個(gè) 64 位原子加操作。
由于文檔不足,要獲取更多信息和例子,目前可能只能參考 LLVM 源碼中的 lib/Target/BPF/BPFInstrInfo.td 以及 test/CodeGen/BPF/。測(cè)試代碼:
#include <linux/bpf.h>
#ifndef __section
# define __section(NAME) \
__attribute__((section(NAME), used))
#endif
__section("prog")
int xdp_test(struct xdp_md *ctx)
{
__u64 a=2, b=3, *c=&a;
/* just a toy xadd example to show the syntax */
asm volatile("lock *(u64 *)(%0+0) +=%1" : "=r"(c) : "r"(b), "0"(c));
return a;
}
char __license[] __section("license")="GPL";
上面的程序會(huì)被編譯成下面的 BPF 指令序列:
Verifier analysis:
0: (b7) r1=2
1: (7b) *(u64 *)(r10 -8)=r1
2: (b7) r1=3
3: (bf) r2=r10
4: (07) r2 +=-8
5: (db) lock *(u64 *)(r2 +0) +=r1
6: (79) r0=*(u64 *)(r10 -8)
7: (95) exit
processed 8 insns (limit 131072), stack depth 8
現(xiàn)代編譯器默認(rèn)會(huì)對(duì)數(shù)據(jù)結(jié)構(gòu)進(jìn)行內(nèi)存對(duì)齊,以實(shí)現(xiàn)更加高效的訪問。結(jié)構(gòu)體成員會(huì)被對(duì)齊到數(shù)倍于其自身大小的內(nèi)存位置,不足的部分會(huì)進(jìn)行填充,因此結(jié)構(gòu)體最終的大小可能會(huì)比預(yù)想中大。
struct called_info {
u64 start; // 8-byte
u64 end; // 8-byte
u32 sector; // 4-byte
}; // size of 20-byte ?
printf("size of %d-byte\n", sizeof(struct called_info)); // size of 24-byte
// Actual compiled composition of struct called_info
// 0x0(0) 0x8(8)
// ↓________________________↓
// | start (8) |
// |________________________|
// | end (8) |
// |________________________|
// | sector(4) | PADDING | <=address aligned to 8
// |____________|___________| with 4-byte PADDING.
內(nèi)核中的 BPF 校驗(yàn)器會(huì)檢查棧邊界,BPF 程序不會(huì)訪問棧邊界外的空間,或者是未初始化的棧空間。如果將結(jié)構(gòu)體中填充出來的內(nèi)存區(qū)域作為一個(gè) map 值進(jìn)行訪問,那調(diào)用 bpf_prog_load() 時(shí)就會(huì)報(bào) invalid indirect read from stack 錯(cuò)誤。
示例代碼:
struct called_info {
u64 start;
u64 end;
u32 sector;
};
struct bpf_map_def SEC("maps") called_info_map={
.type=BPF_MAP_TYPE_HASH,
.key_size=sizeof(long),
.value_size=sizeof(struct called_info),
.max_entries=4096,
};
SEC("kprobe/submit_bio")
int submit_bio_entry(struct pt_regs *ctx)
{
char fmt[]="submit_bio(bio=0x%lx) called: %llu\n";
u64 start_time=bpf_ktime_get_ns();
long bio_ptr=PT_REGS_PARM1(ctx);
struct called_info called_info={
.start=start_time,
.end=0,
.sector=0
};
bpf_map_update_elem(&called_info_map, &bio_ptr, &called_info, BPF_ANY);
bpf_trace_printk(fmt, sizeof(fmt), bio_ptr, start_time);
return 0;
}
通過bpf_load_program() 加載時(shí),會(huì)產(chǎn)生下面的錯(cuò)誤輸出:
bpf_load_program() err=13
0: (bf) r6=r1
...
19: (b7) r1=0
20: (7b) *(u64 *)(r10 -72)=r1
21: (7b) *(u64 *)(r10 -80)=r7
22: (63) *(u32 *)(r10 -64)=r1
...
30: (85) call bpf_map_update_elem#2
invalid indirect read from stack off -80+20 size 24
在 bpf_prog_load() 中會(huì)調(diào)用 BPF 校驗(yàn)器的 bpf_check() 函數(shù),后者會(huì)調(diào)用 check_func_arg() -> check_stack_boundary() 來檢查棧邊界。從上面的錯(cuò)誤可以看出 ,struct called_info 被編譯成 24 字節(jié),錯(cuò)誤信息提示從 +20 位置讀取數(shù)據(jù)是“非法的間接讀取”。從我們更前面給出的內(nèi)存布局圖中可以看到, 地址 0x14(20) 是填充(PADDING )開始的地方。這里再次畫出內(nèi)存布局圖以方便對(duì)比:
// Actual compiled composition of struct called_info
// 0x10(16) 0x14(20) 0x18(24)
// ↓____________↓___________↓
// | sector(4) | PADDING | <=address aligned to 8
// |____________|___________| with 4-byte PADDING.
check_stack_boundary() 會(huì)遍歷每一個(gè)從開始指針出發(fā)的 access_size (24) 字節(jié), 確保它們位于棧邊界內(nèi)部,并且棧內(nèi)的所有元素都初始化了。因此填充的部分是不允許使用的,所以報(bào)了 “invalid indirect read from stack” 錯(cuò)誤。要避免這種錯(cuò)誤,需要將結(jié)構(gòu)體中的填充去掉。這是通過 #pragma pack(n) 原語實(shí)現(xiàn)的:
#pragma pack(4)
struct called_info {
u64 start; // 8-byte
u64 end; // 8-byte
u32 sector; // 4-byte
}; // size of 20-byte ?
printf("size of %d-byte\n", sizeof(struct called_info)); // size of 20-byte
// Actual compiled composition of packed struct called_info
// 0x0(0) 0x8(8)
// ↓________________________↓
// | start (8) |
// |________________________|
// | end (8) |
// |________________________|
// | sector(4) | <=address aligned to 4
// |____________| with no PADDING.
在 struct called_info 前面加上 #pragma pack(4) 之后,編譯器會(huì)以 4 字節(jié)為單位進(jìn)行對(duì)齊。上面的圖可以看到,這個(gè)結(jié)構(gòu)體現(xiàn)在已經(jīng)變成 20 字節(jié)大小,沒有填充了。
但是,去掉填充也是有弊端的。例如,編譯器產(chǎn)生的代碼沒有原來優(yōu)化的好。去掉填充之后 ,處理器訪問結(jié)構(gòu)體時(shí)觸發(fā)的是非對(duì)齊訪問(unaligned access),可能會(huì)導(dǎo)致性能下降。 并且,某些架構(gòu)上的校驗(yàn)器可能會(huì)直接拒絕非對(duì)齊訪問。
不過,我們也有一種方式可以避免產(chǎn)生自動(dòng)填充:手動(dòng)填充。我們簡(jiǎn)單地在結(jié)構(gòu)體中加入一 個(gè) u32 pad 成員來顯式填充,這樣既避免了自動(dòng)填充的問題,又解決了非對(duì)齊訪問的問 題。
struct called_info {
u64 start; // 8-byte
u64 end; // 8-byte
u32 sector; // 4-byte
u32 pad; // 4-byte
}; // size of 24-byte ?
printf("size of %d-byte\n", sizeof(struct called_info)); // size of 24-byte
// Actual compiled composition of struct called_info with explicit padding
// 0x0(0) 0x8(8)
// ↓________________________↓
// | start (8) |
// |________________________|
// | end (8) |
// |________________________|
// | sector(4) | pad (4) | <=address aligned to 8
// |____________|___________| with explicit PADDING.
某些網(wǎng)絡(luò)相關(guān)的 BPF 輔助函數(shù),例如 bpf_skb_store_bytes,可能會(huì)修改包的大小。校驗(yàn)器無法跟蹤這類改動(dòng),因此它會(huì)將所有之前對(duì)包數(shù)據(jù)的引用都視為過期的(未驗(yàn)證的) 。因此,為避免程序被校驗(yàn)器拒絕,在訪問數(shù)據(jù)之外需要先更新相應(yīng)的引用。
來看下面的例子:
struct iphdr *ip4=(struct iphdr *) skb->data + ETH_HLEN;
skb_store_bytes(skb, l3_off + offsetof(struct iphdr, saddr), &new_saddr, 4, 0);
if (ip4->protocol==IPPROTO_TCP) {
// do something
}
校驗(yàn)器會(huì)拒絕這段代碼,因?yàn)樗J(rèn)為在 skb_store_bytes 執(zhí)行之后,引用 ip4->protocol 是未驗(yàn)證的(invalidated):
R1=pkt_end(id=0,off=0,imm=0) R2=pkt(id=0,off=34,r=34,imm=0) R3=inv0
R6=ctx(id=0,off=0,imm=0) R7=inv(id=0,umax_value=4294967295,var_off=(0x0; 0xffffffff))
R8=inv4294967162 R9=pkt(id=0,off=0,r=34,imm=0) R10=fp0,call_-1
...
18: (85) call bpf_skb_store_bytes#9
19: (7b) *(u64 *)(r10 -56)=r7
R0=inv(id=0) R6=ctx(id=0,off=0,imm=0) R7=inv(id=0,umax_value=2,var_off=(0x0; 0x3))
R8=inv4294967162 R9=inv(id=0) R10=fp0,call_-1 fp-48=mmmm???? fp-56=mmmmmmmm
21: (61) r1=*(u32 *)(r9 +23)
R9 invalid mem access 'inv'
要解決這個(gè)問題,必須更新(重新計(jì)算) ip4 的地址:
蘋果公司作為最賺錢的技術(shù)公司之一,強(qiáng)悍的技術(shù)實(shí)力是飛速發(fā)展并且長(zhǎng)期霸占龍頭老大的強(qiáng)大后盾,本文主要扒一扒蘋果公司的技術(shù)崛起之路,用事實(shí)告訴你前期的技術(shù)積累有多重要!
蘋果公司作為最賺錢的技術(shù)公司之一,強(qiáng)悍的技術(shù)實(shí)力是飛速發(fā)展并且長(zhǎng)期霸占龍頭老大的強(qiáng)大后盾,本文主要扒一扒蘋果公司的技術(shù)崛起之路,用事實(shí)告訴你前期的技術(shù)積累有多重要!
1. 康寧大猩猩玻璃
與蘋果公司的合作,讓康寧這家百年老廠重新煥發(fā)生機(jī),追溯以往,上一個(gè)和康寧玻璃有關(guān)的重要發(fā)明是燈泡:1879 年,托馬斯·愛迪生發(fā)明燈泡的時(shí)候,找到了康寧公司,希望找到一種穩(wěn)定安全的玻璃燈罩。這一次蘋果公司的成功也讓這兩家公司互相成就了。
蘋果設(shè)計(jì)靈魂人物喬納森艾維帶著自己的核心團(tuán)隊(duì)人員來到康寧公司,在康寧小鎮(zhèn)的工廠里和工人們一起燒制玻璃。當(dāng)時(shí),還沒有像今天這么赫赫有名的康寧正在研發(fā)一種更為結(jié)實(shí)的玻璃,如今這個(gè)名字家喻戶曉:康寧大猩猩玻璃。
和蘋果開啟合作的康寧希望,有一天能夠做出足夠堅(jiān)韌的玻璃和陶瓷用在 iPhone 上,這樣 iPhone 就不需要使用金屬外框了。康寧的 CEO 威克斯說:?jiǎn)滩妓购吞O果讓我們更優(yōu)秀,我們所有人都對(duì)自己的產(chǎn)品非常狂熱。
這不是異想天開,《喬布斯傳》記錄了康寧的夢(mèng)想,而偶爾露面的蘋果專利申請(qǐng)也僅僅是掀開這款手機(jī)的一角供人想象。這款 iPhone 機(jī)身沒有任何的開口,包括 SIM 卡開口,耳機(jī)接口,聽筒接口以及音量鍵電源鍵的開口,甚至連充電開口都沒有。
是的,一款天衣無縫,真正渾然一體的全玻璃機(jī)身手機(jī),有金屬中框在玻璃機(jī)身之內(nèi),但你看不到觸不著。
2007年蘋果即將推出第一代iPhone,喬布斯想要讓他的寶貝屏幕完美防刮,力排眾議選擇康寧作為屏幕玻璃供應(yīng)商。康寧第一代大猩猩玻璃一戰(zhàn)成名,在防刮擦和受力強(qiáng)度上有不錯(cuò)的表現(xiàn)。
第二代、第三代康寧大猩猩玻璃加強(qiáng)防刮擦和自身結(jié)構(gòu)強(qiáng)度,同時(shí)也把玻璃做得越來越薄。第四代、第五代大猩猩玻璃把重點(diǎn)放在了跌落性能方面,屏幕朝下從1.6米的高度跌落到粗糙表面上時(shí),玻璃完好率達(dá)到80%
這是喬布斯一次大膽的嘗試,現(xiàn)在防刮花的大猩猩玻璃在高端智能手機(jī)、平板和電腦上被廣泛應(yīng)用,多家國(guó)際大廠的液晶電視上也使用了這種玻璃。
2. 語音助手siri
而siri作為這個(gè)款手機(jī)的最大賣點(diǎn),其技術(shù)手段來自于一個(gè)叫 Siri Inc的公司。而這個(gè)公司,則來自于世界最大規(guī)模的人工智能項(xiàng)目--五角大樓的CALO(Cognitive Assistant that Learns and Organizes)。Siri Inc這家公司成立于2007年,專為蘋果用戶提供語音數(shù)字個(gè)人助理服務(wù)。蘋果公司于2010年4月份將其收購,此后,蘋果于2011年在iPhone 4S上推出Siri語音助理,將其整合到iOS移動(dòng)操作系統(tǒng)中。
當(dāng)時(shí)負(fù)責(zé)這個(gè)項(xiàng)目的是SRI也就是斯坦福研究院(也是曾經(jīng)發(fā)明了鼠標(biāo)的研究院)。后來SRI從CALO獨(dú)立出來,在09年的時(shí)候,被蘋果以兩億美元的價(jià)格收購了。在這之后,蘋果手機(jī)開始開發(fā)搭載SIRI的技術(shù),并且在IPhone4上取得了成功!2011年的時(shí)候,IPhone4發(fā)售,這也是IPhone4開始制霸中國(guó)市場(chǎng)的開始!
如今的Siri無人不知,雖說實(shí)用性上還不能讓用戶離不開它,但幾年間Siri的成長(zhǎng)也十分迅速。去年,Siri與國(guó)內(nèi)百度公司達(dá)成協(xié)議,國(guó)內(nèi)用戶使用Siri時(shí)將會(huì)呈現(xiàn)百度百科中的內(nèi)容,讓Siri在國(guó)內(nèi)實(shí)用性大幅提升。其實(shí)Siri這家在美國(guó)的聲音控制公司早在2010年就被蘋果收購,并在上出現(xiàn)并成為中的重要功能。而這之后,一家名為Novauris Technologies的英國(guó)公司也被蘋果收購,用來繼續(xù)完善Siri。
之后,喬布斯對(duì)此還不滿意,于是蘋果一方面自己研發(fā)提高Siri的性能,另一方面在2014年4月,蘋果收購的Novauris是一家自動(dòng)化語音識(shí)別技術(shù)公司,他們的核心產(chǎn)品是基于服務(wù)器的可擴(kuò)展語音識(shí)別系統(tǒng)NovaSystem,特點(diǎn)是能夠同時(shí)處理多個(gè)語音訪問請(qǐng)求;又在2015年10月,蘋果通過收購VocalIQ以解決Siri只能識(shí)別最基本的指令,無法做出匹配度較高回答的問題。
VocalIQ能夠利用深度學(xué)習(xí)來理解語言的環(huán)境,從而使得人機(jī)對(duì)話變得更加自然。此后,。VocalIQ 的技術(shù)主要會(huì)應(yīng)用在改善 Siri 的方面,增加用戶的使用粘性。VocalIQ 專注于自然語言處理技術(shù),這意味著它的產(chǎn)品可以讓電腦更輕松地理解人類所發(fā)出的指令,也就是說它能夠讓 Siri 變得更加智能。開發(fā)者在Siri中嵌入VocalIQ的人工智能技術(shù)后,利用這個(gè)平臺(tái)來儲(chǔ)存和學(xué)習(xí)用戶的交流信息,實(shí)現(xiàn)準(zhǔn)確識(shí)別用戶的指令并提供更加智能的對(duì)話。除了能夠識(shí)別語音之外,VocalIQ 的產(chǎn)品還能夠幫助應(yīng)用更好地學(xué)習(xí),也就是說你使用 Siri 的次數(shù)越多,Siri 得到的鍛煉也就越多。
3. 手機(jī)指紋識(shí)別功能
早在1998年,就有手機(jī)指紋識(shí)別功能。當(dāng)時(shí)的西門子公司,用這各項(xiàng)技術(shù)生產(chǎn)了原型機(jī)。但是對(duì)于當(dāng)時(shí)大多數(shù)的手機(jī)用戶來說,這個(gè)功能基本沒有什么作用,因此,最后這款手機(jī)也是沒有得到規(guī)模推廣。
在此之后,一家法國(guó)的公司,量產(chǎn)了第一部具有指紋識(shí)別功能手機(jī)。但是大家都知道,那個(gè)年代的手機(jī),并不如現(xiàn)在的這樣,有很多的個(gè)人隱私。那個(gè)年代的手機(jī)功能單一,基本上就是用來通訊的。同樣的,這款手機(jī),沒有什么市場(chǎng),最終停產(chǎn)。
2012年,蘋果以3.56億美元收購指紋傳感器廠商AuthenTec,當(dāng)時(shí),他們與一個(gè)瑞典的名叫FPC的公司在相互競(jìng)爭(zhēng)的過程中,不斷地研究指紋技術(shù),不過那時(shí)候因?yàn)闀r(shí)代的局限性,指紋技術(shù)并沒有多大的進(jìn)展。一直到2012年AuthenTec才研發(fā)出來了正面按壓式指紋技術(shù)。這個(gè)技術(shù)成熟之后,AuthenTec立刻向向 LG、富士、三星、諾基亞、蘋果等大客戶展示,然而最終只有蘋果感興趣,迅速收購了他們并結(jié)合 iPhone 獨(dú)特的圓形 HOME 鍵,最終變成了今天的 Touch ID。
隨后一個(gè)成熟的指紋加密解決方案在iPhone 5s上亮相。得益于蘋果對(duì)于整個(gè)產(chǎn)業(yè)的帶動(dòng)性以及其自身方便高效的使用方式,促使這一功能隨后迅速普及到Android系統(tǒng)平臺(tái)的手機(jī),如今它甚至來到了千元機(jī)上。指紋傳感器廠商AuthenTec是蘋果收購科技公司為自己所用的一個(gè)經(jīng)典案例。
通過收購這家公司,蘋果的指紋識(shí)功能也開始在手機(jī)領(lǐng)域成了特色。iPhone 5s從上市就開始得到廣泛的關(guān)注,首搭指紋識(shí)別系統(tǒng)無疑起到了舉足輕重的作用。Touch ID一改此前密碼、圖案解鎖等繁瑣方式,帶來了更加安全便捷的使用方式。指紋識(shí)別還應(yīng)用在支付購物等關(guān)鍵應(yīng)用上,為支付安全提供完善的解決方案。
4. 3D Touch 技術(shù)
據(jù)透露,這項(xiàng)技術(shù),在蘋果已經(jīng)準(zhǔn)備了五年之久,一直在研發(fā)和調(diào)制。也就是說,早在IP4的時(shí)代,這項(xiàng)技術(shù)就已經(jīng)開始了!相對(duì)于早先的觸屏技術(shù),3D Touch 技術(shù)顯然是一場(chǎng)革命。并且最終,在IP6S上實(shí)現(xiàn)了這個(gè)功能。
說實(shí)在的,在沒有使用之前,很少有消費(fèi)者有這種概念。但是讓你使用之后,你就會(huì)發(fā)現(xiàn),這個(gè)技術(shù),真的讓人的人機(jī)交互變得更加的有趣和實(shí)用。
蘋果公司其實(shí)一直在收購各種各樣的公司。而收購的唯一目的,自然就是讓自己的產(chǎn)品更加出色了。
聯(lián)系實(shí)際工程師的發(fā)展來說:這一點(diǎn)跟工程師的技術(shù)積累是一樣的,通常前期需要大量的學(xué)習(xí)以及練習(xí),例如我們非常熟悉的10000小時(shí)理論,才能在后期集中爆發(fā)出來,完成量變到質(zhì)變的超越。企業(yè)成功的背后,包含著大量的技術(shù)及管理的積累,個(gè)人職業(yè)的成功又何嘗不是如此呢?