Skip to content

[RFC][BPF] Support Jump Table #133856

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

Conversation

yonghong-song
Copy link
Contributor

@yonghong-song yonghong-song commented Apr 1, 2025

NOTE: We probably need cpu v5 or other flags to enable this feature. We can add it later when necessary.

This patch adds jump table support. A new insn 'gotox ' is added to allow goto through a register. The register represents the address in the current section. The function is a concrete example with bpf selftest progs/user_ringbuf_success.c.

Compilation command line to generate .s file:

clang  -g -Wall -Werror -D__TARGET_ARCH_x86 -mlittle-endian \
    -I/home/yhs/work/bpf-next/tools/testing/selftests/bpf/tools/include \
    -I/home/yhs/work/bpf-next/tools/testing/selftests/bpf \
    -I/home/yhs/work/bpf-next/tools/include/uapi \
    -I/home/yhs/work/bpf-next/tools/testing/selftests/usr/include -std=gnu11 \
    -fno-strict-aliasing -Wno-compare-distinct-pointer-types \
    -idirafter /home/yhs/work/llvm-project/llvm/build.21/Release/lib/clang/21/include \
    -idirafter /usr/local/include -idirafter /usr/include \
    -DENABLE_ATOMICS_TESTS   -O2 -S progs/user_ringbuf_success.c \
    -o /home/yhs/work/bpf-next/tools/testing/selftests/bpf/user_ringbuf_success.bpf.o.s \
    --target=bpf -mcpu=v3

The related assembly:

  read_protocol_msg:
        ...
        r3 <<= 3
        r1 = .LJTI1_0 ll
        r1 += r3
        r1 = *(u64 *)(r1 + 0)
        gotox r1
  LBB1_4:
        r1 = *(u64 *)(r0 + 8)
        goto LBB1_5
  LBB1_7:
        r1 = *(u64 *)(r0 + 8)
        goto LBB1_8
  LBB1_9:
        w1 = *(u32 *)(r0 + 8)
        r1 <<= 32
        r1 s>>= 32
        r2 = kern_mutated ll
        r3 = *(u64 *)(r2 + 0)
        r3 *= r1
        *(u64 *)(r2 + 0) = r3
        goto LBB1_11
  LBB1_6:
        w1 = *(u32 *)(r0 + 8)
        r1 <<= 32
        r1 s>>= 32
  LBB1_5:
  ...
        .section        .rodata,"a",@progbits
        .p2align        3, 0x0
  .LJTI1_0:
        .quad   LBB1_4
        .quad   LBB1_6
        .quad   LBB1_7
        .quad   LBB1_9
  ...
  publish_next_kern_msg:
        ...
        r6 <<= 3
        r1 = .LJTI6_0 ll
        r1 += r6
        r1 = *(u64 *)(r1 + 0)
        gotox r1
  LBB6_3:
        ...
  LBB6_5:
        ...
  LBB6_6:
        ...
  LBB6_4:
        ...
        .section        .rodata,"a",@progbits
        .p2align        3, 0x0
.LJTI6_0:
        .quad   LBB6_3
        .quad   LBB6_4
        .quad   LBB6_5
        .quad   LBB6_6

You can see in the above .LJTI1_0 and .LJTI6_0 are actually jump table targets
and these two jump tables are used in insns so they can get proper jump
table target with gotox insn.

Now let us look at sections in .o file

For example,

  [ 6] .rodata           PROGBITS        0000000000000000 000740 0000d6 00   A  0   0  8
  [ 7] .rel.rodata       REL             0000000000000000 003860 000080 10   I 39   6  8
  [ 8] .llvm_jump_table_sizes LLVM_JT_SIZES 0000000000000000 000816 000010 00      0   0  1
  [ 9] .rel.llvm_jump_table_sizes REL    0000000000000000 0038e0 000010 10   I 39   8  8
  ...
  [14] .llvm_jump_table_sizes LLVM_JT_SIZES 0000000000000000 000958 000010 00      0   0  1
  [15] .rel.llvm_jump_table_sizes REL    0000000000000000 003970 000010 10   I 39  14  8

With llvm-readelf dump section 8 and 14:

  $ llvm-readelf -x 8 user_ringbuf_success.bpf.o
  Hex dump of section '.llvm_jump_table_sizes':
  0x00000000 00000000 00000000 04000000 00000000 ................
  $ llvm-readelf -x 14 user_ringbuf_success.bpf.o
  Hex dump of section '.llvm_jump_table_sizes':
  0x00000000 20000000 00000000 04000000 00000000  ...............

You can see. There are two jump tables:

  jump table 1: offset 0, size 4 (4 labels)
  jump table 2: offset 0x20, size 4 (4 labels)

Check sections 9 and 15, we can find the corresponding section:

  Relocation section '.rel.llvm_jump_table_sizes' at offset 0x38e0 contains 1 entries:
      Offset             Info             Type               Symbol's Value  Symbol's Name
  0000000000000000  0000000a00000002 R_BPF_64_ABS64         0000000000000000 .rodata
  Relocation section '.rel.llvm_jump_table_sizes' at offset 0x3970 contains 1 entries:
      Offset             Info             Type               Symbol's Value  Symbol's Name
  0000000000000000  0000000a00000002 R_BPF_64_ABS64         0000000000000000 .rodata

and confirmed that the relocation is against '.rodata'.

Dump .rodata section:

  0x00000000 a8000000 00000000 10010000 00000000 ................
  0x00000010 b8000000 00000000 c8000000 00000000 ................
  0x00000020 28040000 00000000 00050000 00000000 (...............
  0x00000030 70040000 00000000 b8040000 00000000 p...............
  0x00000040 44726169 6e207265 7475726e 65643a20 Drain returned:

So we can get two jump tables:

  .rodata offset 0, # of lables 4:
  0x00000000 a8000000 00000000 10010000 00000000 ................
  0x00000010 b8000000 00000000 c8000000 00000000 ................
  .rodata offset 0x200, # of lables 4:
  0x00000020 28040000 00000000 00050000 00000000 (...............
  0x00000030 70040000 00000000 b8040000 00000000 p...............

This way, you just need to scan related code section. As long as it
matches one of jump tables (.rodata relocation, offset also matching),
you do not need to care about gotox at all in libbpf.

An option -bpf-min-jump-table-entries is implemented to control the minimum
number of entries to use a jump table on BPF. The default value 4, but it
can be changed with the following clang option

  clang ... -mllvm -bpf-min-jump-table-entries=6

where the number of jump table cases needs to be >= 6 in order to
use jump table.

@yonghong-song
Copy link
Contributor Author

@aspsk As we discussed in LSFMMBPF, here is the implementation for llvm jump table support. Please take a look and try libbpf/kernel implementations. Let me know if you hit any issues.

@4ast
Copy link
Member

4ast commented Apr 1, 2025

I could explore to use pc relative

Don't bother. x86 is doing it to save a byte in encoding. This technique doesn't apply to bpf isa.


let isIndirectBranch = 1 in {
def JX : JMP_IND<BPF_JA, "gotox", [(brind i64:$dst)]>;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice to see how it should be done, I just had hardcoded it in my test branch: aspsk@98773c6

Copy link

@aspsk aspsk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @yonghong-song! I will test this, match with the verification part, and post my results in this PR

@@ -65,10 +65,11 @@ BPFTargetLowering::BPFTargetLowering(const TargetMachine &TM,

setOperationAction(ISD::BR_CC, MVT::i64, Custom);
setOperationAction(ISD::BR_JT, MVT::Other, Expand);
setOperationAction(ISD::BRIND, MVT::Other, Expand);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, this does remove restriction to not produce indirect jumps?

Is there a way to control if we want to generate indirect jumps "in general" vs., say, "only for large switches"? (Or even only for a particular switch?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, this does remove restriction to not produce indirect jumps?
Yes, we do not want to expand 'brind', rather we will do pattern matching with 'brind'.

Is there a way to control if we want to generate indirect jumps "in general" vs., say, "only for large switches"? (Or even only for a particular switch?)

Good point. Let me do some experiments with a flag for this. I am not sure whether I could do 'only for a particular switch', but I will do some investigation. Hopefully can find a s solution for that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an option to control how many cases in a switch statement to use jump table. The default is 4 cases. But you can change it with additional clang option, e.g., the minimum number of cases must be 6, then

clang ... -mllvm -bpf-min-jump-table-entries=6

I checked other targets, there are no control for a specific switch. So I think we do not need them for now.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thanks!

@aspsk
Copy link

aspsk commented Apr 1, 2025

With the above information, the size for each sub-rodata can be found easily

@yonghong-song could you please elaborate on this? How exactly is to classify those into per-table?

@yonghong-song
Copy link
Contributor Author

With the above information, the size for each sub-rodata can be found easily

@yonghong-song could you please elaborate on this? How exactly is to classify those into per-table?

The below is an example for test_tc_tunnel.bpf.o with -mllvm -bpf-min-jump-table-entries=1. The purpose is to have more jump tables in the code. First you can check '.rel.rodata' section. The following is the information with llvm-readelf -r test_tc_tunnel.bpf.o:

Relocation section '.rel.text' at offset 0xdd40 contains 9 entries:                                                            
    Offset             Info             Type               Symbol's Value  Symbol's Name                                       
0000000000000038  0000001700000001 R_BPF_64_64            0000000000000000 .rodata                                             
0000000000000120  0000001700000001 R_BPF_64_64            0000000000000000 .rodata                                             
0000000000000170  0000001700000001 R_BPF_64_64            0000000000000000 .rodata                                             
0000000000000268  0000001700000001 R_BPF_64_64            0000000000000000 .rodata                                             
0000000000000330  0000001700000001 R_BPF_64_64            0000000000000000 .rodata                                             
00000000000003a8  0000001700000001 R_BPF_64_64            0000000000000000 .rodata                                             
00000000000003e0  0000001700000001 R_BPF_64_64            0000000000000000 .rodata                                             
0000000000000420  0000001700000001 R_BPF_64_64            0000000000000000 .rodata                                             
0000000000000460  0000001700000001 R_BPF_64_64            0000000000000000 .rodata                                             
                                                                                                                               
Relocation section '.reldecap' at offset 0xddd0 contains 3 entries:                                                            
    Offset             Info             Type               Symbol's Value  Symbol's Name                                       
0000000000000048  0000001700000001 R_BPF_64_64            0000000000000000 .rodata                                             
00000000000000f0  0000001700000001 R_BPF_64_64            0000000000000000 .rodata                                             
00000000000001a0  000000020000000a R_BPF_64_32            0000000000000000 .text 

The above .rodata is what you really care. You can also find all .rodata relocations happen in decap and .text sections.

Relocation section '.rel.rodata' at offset 0xde00 contains 43 entries:
    Offset             Info             Type               Symbol's Value  Symbol's Name
0000000000000000  0000001500000002 R_BPF_64_ABS64         0000000000000000 decap
0000000000000008  0000001500000002 R_BPF_64_ABS64         0000000000000000 decap
0000000000000010  0000000200000002 R_BPF_64_ABS64         0000000000000000 .text
0000000000000018  0000000200000002 R_BPF_64_ABS64         0000000000000000 .text
0000000000000020  0000000200000002 R_BPF_64_ABS64         0000000000000000 .text
0000000000000028  0000000200000002 R_BPF_64_ABS64         0000000000000000 .text
...
0000000000000150  0000000200000002 R_BPF_64_ABS64         0000000000000000 .text

You then need to go through sections 'decap' and '.text' for their .rodata relocations.
For example, for

0000000000000038  0000001700000001 R_BPF_64_64            0000000000000000 .rodata                                             

It corresponds to insn 7 (0x38/8 = 7).

       7:       18 03 00 00 80 00 00 00 00 00 00 00 00 00 00 00 r3 = 0x80 ll
                0000000000000038:  R_BPF_64_64  .rodata

In the above 'r3 = 0x80' means the relocation starts 0x80 at .rodata section.

You need to scan ALL such relocations in .text and decap sections and with that you can sort based on start of each relocation. After that, you will be able to calculate each relocation size.

After you calculated each relocation size (for .rodata section), you need to check whether a particular relocation is for gotox or something else. So you need to go backwords to scan. For example,

      35:       67 03 00 00 03 00 00 00 r3 <<= 0x3
      36:       18 02 00 00 40 01 00 00 00 00 00 00 00 00 00 00 r2 = 0x140 ll
                0000000000000120:  R_BPF_64_64  .rodata
      38:       0f 32 00 00 00 00 00 00 r2 += r3
      39:       79 22 00 00 00 00 00 00 r2 = *(u64 *)(r2 + 0x0)
      40:       0d 02 00 00 00 00 00 00 gotox r2

You find a gotox insn with target r2, then you need to go back and find 'r2 = *(u64 *)(r2 + 0x0)' and then 'r2 += r3' and then 'r2 = 0x140 ll'. The above code pattern is gernated by llvm and should be generally true for jump table implementation. And you will be certain that the table for this particular gotox will be in offset 0x140 of .rodata section. The size of the table is already calculated based on the previous mechanism by scanning all .rodata relocations in .text and decap sections.

@aspsk
Copy link

aspsk commented Apr 2, 2025

You find a gotox insn with target r2, then you need to go back and find 'r2 = *(u64 *)(r2 + 0x0)' and then 'r2 += r3' and then 'r2 = 0x140 ll'. The above code pattern is gernated by llvm and should be generally true for jump table implementation. And you will be certain that the table for this particular gotox will be in offset 0x140 of .rodata section. The size of the table is already calculated based on the previous mechanism by scanning all .rodata relocations in .text and decap sections.

I am looking into how to automate this properly (I have a really hacky PoC test working with this version of llvm and my custom test). It looks simpler with explicit jump tables (when I take an address of a label and store in an array), because then I can just push values to a custom section.

Will post updates here.

@yonghong-song
Copy link
Contributor Author

I find a llvm option -emit-jump-table-sizes-section which can dump the jump table section/offset/size directly. This will avoid the gotox -> jump table address analysis.
The following is some details on commit 3:

    For example,
      [ 6] .rodata           PROGBITS        0000000000000000 000740 0000d6 00   A  0   0  8
      [ 7] .rel.rodata       REL             0000000000000000 003860 000080 10   I 39   6  8
      [ 8] .llvm_jump_table_sizes LLVM_JT_SIZES 0000000000000000 000816 000010 00      0   0  1
      [ 9] .rel.llvm_jump_table_sizes REL    0000000000000000 0038e0 000010 10   I 39   8  8
      ...
      [14] .llvm_jump_table_sizes LLVM_JT_SIZES 0000000000000000 000958 000010 00      0   0  1
      [15] .rel.llvm_jump_table_sizes REL    0000000000000000 003970 000010 10   I 39  14  8
    
    With llvm-readelf dump section 8 and 14:
      $ llvm-readelf -x 8 user_ringbuf_success.bpf.o
      Hex dump of section '.llvm_jump_table_sizes':
      0x00000000 00000000 00000000 04000000 00000000 ................
      $ llvm-readelf -x 14 user_ringbuf_success.bpf.o
      Hex dump of section '.llvm_jump_table_sizes':
      0x00000000 20000000 00000000 04000000 00000000  ...............
    You can see. There are two jump tables:
      jump table 1: offset 0, size 4 (4 labels)
      jump table 2: offset 0x20, size 4 (4 labels)
    
    Check sections 9 and 15, we can find the corresponding section:
      Relocation section '.rel.llvm_jump_table_sizes' at offset 0x38e0 contains 1 entries:
          Offset             Info             Type               Symbol's Value  Symbol's Name
      0000000000000000  0000000a00000002 R_BPF_64_ABS64         0000000000000000 .rodata
      Relocation section '.rel.llvm_jump_table_sizes' at offset 0x3970 contains 1 entries:
          Offset             Info             Type               Symbol's Value  Symbol's Name
      0000000000000000  0000000a00000002 R_BPF_64_ABS64         0000000000000000 .rodata
    and confirmed that the relocation is against '.rodata'.

    Dump .rodata section:
      0x00000000 a8000000 00000000 10010000 00000000 ................
      0x00000010 b8000000 00000000 c8000000 00000000 ................
      0x00000020 28040000 00000000 00050000 00000000 (...............
      0x00000030 70040000 00000000 b8040000 00000000 p...............
      0x00000040 44726169 6e207265 7475726e 65643a20 Drain returned:
    
    So we can get two jump tables:
      .rodata offset 0, # of lables 4:
      0x00000000 a8000000 00000000 10010000 00000000 ................
      0x00000010 b8000000 00000000 c8000000 00000000 ................
      .rodata offset 0x200, # of lables 4:
      0x00000020 28040000 00000000 00050000 00000000 (...............
      0x00000030 70040000 00000000 b8040000 00000000 p...............

This way, you just need to scan related code section. As long as it
matches one of jump tables (.rodata relocation, offset also matching),
you do not need to care about gotox at all.

@yonghong-song
Copy link
Contributor Author

This is one test failure like below:

.---command stderr------------
# | C:\ws\src\llvm\test\CodeGen\X86\jump-table-size-section.ll:86:15: error: NOFLAG-NOT: excluded string found in input
# | ; NOFLAG-NOT: .section .llvm_jump_table_sizes
# |               ^
# | <stdin>:41:2: note: found here
# |  .section .llvm_jump_table_sizes,"G",@llvm_jt_sizes,foo1,comdat
# |  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# | C:\ws\src\llvm\test\CodeGen\X86\jump-table-size-section.ll:154:15: error: NOFLAG-NOT: excluded string found in input
# | ; NOFLAG-NOT: .section .llvm_jump_table_sizes
# |               ^
# | <stdin>:150:2: note: found here
# |  .section .llvm_jump_table_sizes,"",@llvm_jt_sizes
# |  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# | 
# | Input file: <stdin>
# | Check file: C:\ws\src\llvm\test\CodeGen\X86\jump-table-size-section.ll

The reason should be due to my unconditional enabling -emit-jump-table-sizes-section. Will fix in the next revision if this approach is okay for @aspsk

@aspsk
Copy link

aspsk commented Apr 3, 2025

Thanks @yonghong-song, that size/offset section is really useful! This looks sufficient for me to continue with a PoC.

you do not need to care about gotox at all.

Unfortunately, I do, this is required for verification. For indirect jumps to work, two things should be verified:

    rX <- jump_table_x[i] # here jump_table_x[i] should be converted to M[i]
    ...
    gotox rX              # here on load imm=fd(M)

The gotox should reference an instance of an instruction set map M directly. This is required to 1) verify that we take each branch in visit_insn 2) verify the instruction when we symbolically run the verifier. In the latter case gotox rX is allowed iff imm=M and rX was loaded from the same map M.

So, in order to construct a verifiable program, libbpf should:

  • find all jump tables, for each jump table X:
    • create an instruction set map M(X)
    • find all loads from table X, replace by a map value load from map M(X)
  • find all gotox, backtrack the register load from map M(X), set imm=M(X) in the gotox

(Haven't checked yet for real, but this looks to be enough for "custom", e.g., user-defined, jump tables to work. Just declare it as static const, initialize with label addresses, and it will be present in .rodata. Maybe the only change that the corresponding size section should be pushed manually in this case?)

@yonghong-song
Copy link
Contributor Author

Thanks @yonghong-song, that size/offset section is really useful! This looks sufficient for me to continue with a PoC.

you do not need to care about gotox at all.

Unfortunately, I do, this is required for verification. For indirect jumps to work, two things should be verified:

You are right. Verification does need to connect jump table map and gotox insn.
When I say 'don't care gotox at all', I actually mean libbpf part where you need to go through various elf sections to connect them together.

    rX <- jump_table_x[i] # here jump_table_x[i] should be converted to M[i]
    ...
    gotox rX              # here on load imm=fd(M)

The gotox should reference an instance of an instruction set map M directly. This is required to 1) verify that we take each branch in visit_insn 2) verify the instruction when we symbolically run the verifier. In the latter case gotox rX is allowed iff imm=M and rX was loaded from the same map M.

So, in order to construct a verifiable program, libbpf should:

* find all jump tables, for each jump table `X`:
  
  * create an instruction set map `M(X)`
  * find all loads from table `X`, replace by a map value load from map `M(X)`

* find all `gotox`, backtrack the register load from map `M(X)`, set `imm=M(X)` in the `gotox`

Backtrack certainly work. But maybe there is an alternative not to do backtrack.
For example,

        r1 = .LJTI6_0 ll
            <== we will know this is from a map (representing a jump table)
            <== so mark r1 with some info like (jump_table_addr, offset 0)
        r1 += r6
            <== r1 still like jump_table_addr, need to ensure [r6, r6 + 8] is 
                    within jump table range
        r1 = *(u64 *)(r1 + 0)
            <== r1 will be jump_table target, but still keep jump_table reference in r1.
        gotox r1
            <== goto jump_table_target (still has jump_table reference,
                    from verifier perspective, all jump table targets
                    in jump table should be verified).

(Haven't checked yet for real, but this looks to be enough for "custom", e.g., user-defined, jump tables to work. Just declare it as static const, initialize with label addresses, and it will be present in .rodata. Maybe the only change that the corresponding size section should be pushed manually in this case?)

Your user-defined jump table may work. But it would be great if we can just allow the current common switch statements from code cleanness and developer productivity.

@aspsk
Copy link

aspsk commented Apr 3, 2025

Backtrack certainly work. But maybe there is an alternative not to do backtrack. For example,

        r1 = .LJTI6_0 ll
            <== we will know this is from a map (representing a jump table)
            <== so mark r1 with some info like (jump_table_addr, offset 0)
        r1 += r6
            <== r1 still like jump_table_addr, need to ensure [r6, r6 + 8] is 
                    within jump table range
        r1 = *(u64 *)(r1 + 0)
            <== r1 will be jump_table target, but still keep jump_table reference in r1.
        gotox r1
            <== goto jump_table_target (still has jump_table reference,
                    from verifier perspective, all jump table targets
                    in jump table should be verified).

Right, this is exactly what I've meant by "backtrack". Looks like for switch this will look similar in most cases. The need to ensure [r6, r6 + 8] is within jump table range[r6, r6 + 8] from libbpf side might be "best effort", only the verifier needs to check this for sure. I think that from libbpf side "r1 was loaded from map, then an offset was added" is enough to think that r1 is still a valid pointer from the same map. (The devil is in details, of course, let's see for sure when I have code.)

@yonghong-song
Copy link
Contributor Author

Backtrack certainly work. But maybe there is an alternative not to do backtrack. For example,

        r1 = .LJTI6_0 ll
            <== we will know this is from a map (representing a jump table)
            <== so mark r1 with some info like (jump_table_addr, offset 0)
        r1 += r6
            <== r1 still like jump_table_addr, need to ensure [r6, r6 + 8] is 
                    within jump table range
        r1 = *(u64 *)(r1 + 0)
            <== r1 will be jump_table target, but still keep jump_table reference in r1.
        gotox r1
            <== goto jump_table_target (still has jump_table reference,
                    from verifier perspective, all jump table targets
                    in jump table should be verified).

Right, this is exactly what I've meant by "backtrack". Looks like for switch this will look similar in most cases. The need to ensure [r6, r6 + 8] is within jump table range[r6, r6 + 8] from libbpf side might be "best effort", only the verifier needs to check this for sure. I think that from libbpf side "r1 was loaded from map, then an offset was added" is enough to think that r1 is still a valid pointer from the same map. (The devil is in details, of course, let's see for sure when I have code.)

Yes, libbpf does not need to do verifier work. The range analysis should be done in verifier.

@aspsk
Copy link

aspsk commented Apr 14, 2025

Hi @yonghong-song!

I was trying different switch variants, simple ones work like magic, so we're definitely going the right direction.

One simple case fails for me though. Namely, in the example below LLVM generates an unreachable instruction. Could you take a look please? An example source program is

SEC("syscall") int foo(struct simple_ctx *ctx)
{
        switch (ctx->x) {
        case 0:
                ret_user = 2;
                break;
        case 11:
                ret_user = 3;
                break;
        case 27:
                ret_user = 4;
                break;
        case 31:
                ret_user = 5;
                break;
        default:
                ret_user = 19;
                break;
        }

        return 0;
}

Then the object file looks like

0000000000000700 <foo>:
;       switch (ctx->x) {
     224:       79 11 00 00 00 00 00 00 r1 = *(u64 *)(r1 + 0x0)
     225:       25 01 0f 00 1f 00 00 00 if r1 > 0x1f goto +0xf <foo+0x88>
     226:       67 01 00 00 03 00 00 00 r1 <<= 0x3
     227:       18 02 00 00 a8 00 00 00 00 00 00 00 00 00 00 00 r2 = 0xa8 ll
                0000000000000718:  R_BPF_64_64  .rodata
     229:       0f 12 00 00 00 00 00 00 r2 += r1
     230:       79 21 00 00 00 00 00 00 r1 = *(u64 *)(r2 + 0x0)
     231:       0d 01 00 00 00 00 00 00 gotox r1
     232:       05 00 08 00 00 00 00 00 goto +0x8 <foo+0x88>
     233:       b7 01 00 00 02 00 00 00 r1 = 0x2
;       switch (ctx->x) {
     234:       05 00 07 00 00 00 00 00 goto +0x7 <foo+0x90>
     235:       b7 01 00 00 04 00 00 00 r1 = 0x4
;               break;
     236:       05 00 05 00 00 00 00 00 goto +0x5 <foo+0x90>
     237:       b7 01 00 00 03 00 00 00 r1 = 0x3
;               break;
     238:       05 00 03 00 00 00 00 00 goto +0x3 <foo+0x90>
     239:       b7 01 00 00 05 00 00 00 r1 = 0x5
;               break;
     240:       05 00 01 00 00 00 00 00 goto +0x1 <foo+0x90>
     241:       b7 01 00 00 13 00 00 00 r1 = 0x13
     242:       18 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r2 = 0x0 ll
                0000000000000790:  R_BPF_64_64  ret_user
     244:       7b 12 00 00 00 00 00 00 *(u64 *)(r2 + 0x0) = r1
;       return 0;
     245:       b4 00 00 00 00 00 00 00 w0 = 0x0
     246:       95 00 00 00 00 00 00 00 exit

Now, the jump table is

242, 241, 241, 241, 241, 241, 241, 241,
241, 241, 241, 237, 241, 241, 241, 241,
241, 241, 241, 241, 241, 241, 241, 241,
241, 241, 241, 235, 241, 241, 241, 239

And the check

225:       25 01 0f 00 1f 00 00 00 if r1 > 0x1f goto +0xf <foo+0x88>

makes sure that r1 is always loaded from the jump table.

And this makes the instruction

232:       05 00 08 00 00 00 00 00 goto +0x8 <foo+0x88>

unreachable.

@4ast
Copy link
Member

4ast commented Apr 14, 2025

And this makes the instruction
232: 05 00 08 00 00 00 00 00 goto +0x8 <foo+0x88>
unreachable.

I suspect it won't be easy to avoid this on llvm side. Probably better to teach verifier to ignore those.

@aspsk
Copy link

aspsk commented Apr 15, 2025

And this makes the instruction
232: 05 00 08 00 00 00 00 00 goto +0x8 <foo+0x88>
unreachable.

I suspect it won't be easy to avoid this on llvm side. Probably better to teach verifier to ignore those.

Ok, thanks, will do this for now

@aspsk
Copy link

aspsk commented May 4, 2025

Update. I have a patch for kernel + libbpf which uses this LLVM and which passes all my new selftests + all (but one) standard bpf selftests which are compiled to use gotox (exceptions, cgroup_tcp_skb, cls_redirect, bpf_tcp_ca, bpf_iter_setsockopt, tc_change_tail, net_timestamping, tcpbpf_user, user_ringbuf, tcp_custom_syncookie, tcp_hdr_options).

So far only one selftest fails (tunnel). Once I fix it (and cleanup libbpf part of the series a bit), I will prepare and send RFC.

Copy link

github-actions bot commented May 8, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@yonghong-song
Copy link
Contributor Author

Thanks for the update. When trying your above example

SEC("syscall") int foo(struct simple_ctx *ctx)
...

I found a problem and just added another commit to fix the problem. The issue is due to llvm machine-sink pass. The implementation is similar to X86 (X86InstrInfo::getJumpTableIndex()). See the top commit (commit 4) for more details.

@aspsk
Copy link

aspsk commented May 8, 2025

I found a problem and just added another commit to fix the problem

Thanks @yonghong-song! I will test your latest changes over this weekend.

(The tunnel test I've fixed already, so all selftests pass now. Now need to replace hacks with normal code, and will send the patch.)

Copy link
Member

@inclyc inclyc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to modify the ASMParser also?

static bool isValidIdAtStart(StringRef Name) {
return StringSwitch<bool>(Name.lower())
.Case("if", true)
.Case("call", true)
.Case("callx", true)
.Case("goto", true)

@yonghong-song
Copy link
Contributor Author

Do we need to modify the ASMParser also?

static bool isValidIdAtStart(StringRef Name) {
return StringSwitch<bool>(Name.lower())
.Case("if", true)
.Case("call", true)
.Case("callx", true)
.Case("goto", true)

Right, need to add gotox as well. Will fix. Thanks!

@aspsk
Copy link

aspsk commented Jun 15, 2025

Here's the kernel side which works with this LLVM: https://lore.kernel.org/bpf/[email protected]/

The following selftests contain indirect jumps (and pass):

* cgroup_tcp_skb, cls_redirect, bpf_tcp_ca,
* bpf_iter_setsockopt, tc_change_tail, net_timestamping,
* user_ringbuf, tcp_hdr_options, tunnel, exceptions,
* tcpbpf_user, tcp_custom_syncookie

A new selftest bpf_goto_x specifically tries to test different scenarios.

@yonghong-song
Copy link
Contributor Author

Thanks @aspsk I will also take a look at the kernel patch. Also, the current patch has some conflicts with latest 'main' branch. I will rebase and repost the new llvm patch after doing some testing.

NOTE: We probably need cpu v5 or other flags to enable this feature.
We can add it later when necessary.

This patch adds jump table support. A new insn 'gotox <reg>' is
added to allow goto through a register. The register represents
the address in the current section. The function is a concrete
example with bpf selftest progs/user_ringbuf_success.c.

Compilation command line to generate .s file:
=============================================
clang  -g -Wall -Werror -D__TARGET_ARCH_x86 -mlittle-endian \
    -I/home/yhs/work/bpf-next/tools/testing/selftests/bpf/tools/include \
    -I/home/yhs/work/bpf-next/tools/testing/selftests/bpf \
    -I/home/yhs/work/bpf-next/tools/include/uapi \
    -I/home/yhs/work/bpf-next/tools/testing/selftests/usr/include -std=gnu11 \
    -fno-strict-aliasing -Wno-compare-distinct-pointer-types \
    -idirafter /home/yhs/work/llvm-project/llvm/build.21/Release/lib/clang/21/include \
    -idirafter /usr/local/include -idirafter /usr/include \
    -DENABLE_ATOMICS_TESTS   -O2 -S progs/user_ringbuf_success.c \
    -o /home/yhs/work/bpf-next/tools/testing/selftests/bpf/user_ringbuf_success.bpf.o.s \
    --target=bpf -mcpu=v3

The related assembly:
  read_protocol_msg:
        ...
        r3 <<= 3
        r1 = .LJTI1_0 ll
        r1 += r3
        r1 = *(u64 *)(r1 + 0)
        gotox r1
  LBB1_4:
        r1 = *(u64 *)(r0 + 8)
        goto LBB1_5
  LBB1_7:
        r1 = *(u64 *)(r0 + 8)
        goto LBB1_8
  LBB1_9:
        w1 = *(u32 *)(r0 + 8)
        r1 <<= 32
        r1 s>>= 32
        r2 = kern_mutated ll
        r3 = *(u64 *)(r2 + 0)
        r3 *= r1
        *(u64 *)(r2 + 0) = r3
        goto LBB1_11
  LBB1_6:
        w1 = *(u32 *)(r0 + 8)
        r1 <<= 32
        r1 s>>= 32
  LBB1_5:
  ...
        .section        .rodata,"a",@progbits
        .p2align        3, 0x0
  .LJTI1_0:
        .quad   LBB1_4
        .quad   LBB1_6
        .quad   LBB1_7
        .quad   LBB1_9
  ...
  publish_next_kern_msg:
        ...
        r6 <<= 3
        r1 = .LJTI6_0 ll
        r1 += r6
        r1 = *(u64 *)(r1 + 0)
        gotox r1
  LBB6_3:
        ...
  LBB6_5:
        ...
  LBB6_6:
        ...
  LBB6_4:
        ...
        .section        .rodata,"a",@progbits
        .p2align        3, 0x0
.LJTI6_0:
        .quad   LBB6_3
        .quad   LBB6_4
        .quad   LBB6_5
        .quad   LBB6_6

You can see in the above .LJTI1_0 and .LJTI6_0 are actually jump table targets
and these two jump tables are used in insns so they can get proper jump
table target with gotox insn.

Now let us look at sections in .o file
=======================================
For example,
  [ 6] .rodata           PROGBITS        0000000000000000 000740 0000d6 00   A  0   0  8
  [ 7] .rel.rodata       REL             0000000000000000 003860 000080 10   I 39   6  8
  [ 8] .llvm_jump_table_sizes LLVM_JT_SIZES 0000000000000000 000816 000010 00      0   0  1
  [ 9] .rel.llvm_jump_table_sizes REL    0000000000000000 0038e0 000010 10   I 39   8  8
  ...
  [14] .llvm_jump_table_sizes LLVM_JT_SIZES 0000000000000000 000958 000010 00      0   0  1
  [15] .rel.llvm_jump_table_sizes REL    0000000000000000 003970 000010 10   I 39  14  8

With llvm-readelf dump section 8 and 14:
  $ llvm-readelf -x 8 user_ringbuf_success.bpf.o
  Hex dump of section '.llvm_jump_table_sizes':
  0x00000000 00000000 00000000 04000000 00000000 ................
  $ llvm-readelf -x 14 user_ringbuf_success.bpf.o
  Hex dump of section '.llvm_jump_table_sizes':
  0x00000000 20000000 00000000 04000000 00000000  ...............
You can see. There are two jump tables:
  jump table 1: offset 0, size 4 (4 labels)
  jump table 2: offset 0x20, size 4 (4 labels)

Check sections 9 and 15, we can find the corresponding section:
  Relocation section '.rel.llvm_jump_table_sizes' at offset 0x38e0 contains 1 entries:
      Offset             Info             Type               Symbol's Value  Symbol's Name
  0000000000000000  0000000a00000002 R_BPF_64_ABS64         0000000000000000 .rodata
  Relocation section '.rel.llvm_jump_table_sizes' at offset 0x3970 contains 1 entries:
      Offset             Info             Type               Symbol's Value  Symbol's Name
  0000000000000000  0000000a00000002 R_BPF_64_ABS64         0000000000000000 .rodata
and confirmed that the relocation is against '.rodata'.

Dump .rodata section:
  0x00000000 a8000000 00000000 10010000 00000000 ................
  0x00000010 b8000000 00000000 c8000000 00000000 ................
  0x00000020 28040000 00000000 00050000 00000000 (...............
  0x00000030 70040000 00000000 b8040000 00000000 p...............
  0x00000040 44726169 6e207265 7475726e 65643a20 Drain returned:

So we can get two jump tables:
  .rodata offset 0, # of lables 4:
  0x00000000 a8000000 00000000 10010000 00000000 ................
  0x00000010 b8000000 00000000 c8000000 00000000 ................
  .rodata offset 0x200, # of lables 4:
  0x00000020 28040000 00000000 00050000 00000000 (...............
  0x00000030 70040000 00000000 b8040000 00000000 p...............

This way, you just need to scan related code section. As long as it
matches one of jump tables (.rodata relocation, offset also matching),
you do not need to care about gotox at all in libbpf.

An option -bpf-min-jump-table-entries is implemented to control the minimum
number of entries to use a jump table on BPF. The default value 4, but it
can be changed with the following clang option
  clang ... -mllvm -bpf-min-jump-table-entries=6
where the number of jump table cases needs to be >= 6 in order to
use jump table.
@yonghong-song
Copy link
Contributor Author

Rebased on top of current main branch. No functionality change compared to previous version (in more than a month ago).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants