mirror of
https://github.com/varun-r-mallya/Python-BPF.git
synced 2025-12-31 21:06:25 +00:00
Compare commits
422 Commits
v0.1.3
...
71d005b6b1
| Author | SHA1 | Date | |
|---|---|---|---|
| 71d005b6b1 | |||
| 5d9a29ee8e | |||
| 041e538b53 | |||
| 5413cc793b | |||
| f21837aefe | |||
| 0f5c1fa752 | |||
| de02731ea1 | |||
| c22d85ceb8 | |||
| 2b3c81affa | |||
| 8372111616 | |||
| eb4ee64ee5 | |||
| ce7b170fea | |||
| c499fe7421 | |||
| 8239097fbb | |||
| a4cfc2b7aa | |||
| 69b73003ca | |||
| 11e8e72188 | |||
| d3f0e3b2ef | |||
| a03d3e5d4c | |||
| e1f9ac6ba0 | |||
| 27ab3aaf1e | |||
| b34f7dd68a | |||
| 69d8669e44 | |||
| d4f5a9c36e | |||
| b2a57edf11 | |||
| 20ec307288 | |||
| 0b4c6264a8 | |||
| 6345fcdeff | |||
| 6b41f1fb84 | |||
| 74d8014ade | |||
| 5d0a888542 | |||
| 0042280ff1 | |||
| 7a67041ea3 | |||
| 45e6ce5e5c | |||
| c5f0a2806f | |||
| b0ea93a786 | |||
| fc058c4341 | |||
| 158cc42e1e | |||
| 2a1eabc10d | |||
| e5741562f6 | |||
| 93634a4769 | |||
| 9b8462f1ed | |||
| 785182787c | |||
| 80396c78a6 | |||
| 31645f0316 | |||
| e0ad1bfb0f | |||
| 69bee5fee9 | |||
| 2f1aaa4834 | |||
| 0f6971bcc2 | |||
| 08c0ccf0ac | |||
| 64e44d0d58 | |||
| 3ad1b73c5a | |||
| 105c5a7bd0 | |||
| 933d2a5c77 | |||
| b93f704eb8 | |||
| fa82dc7ebd | |||
| e8026a13bf | |||
| a3b4d09652 | |||
| 4e33fd4a32 | |||
| 2cf68f6473 | |||
| d66e6a6aff | |||
| cd74e896cf | |||
| 207f714027 | |||
| 8774277000 | |||
| 5dcf670f49 | |||
| 8743ea17f3 | |||
| 6bce29b90f | |||
| 321415fa28 | |||
| 8776d7607f | |||
| f8844104a6 | |||
| 3343bedd11 | |||
| 8b7b1c08a5 | |||
| 75d3ad4fe2 | |||
| abbf17748d | |||
| 7c559840f0 | |||
| 06773c895f | |||
| 1e3d775865 | |||
| 168e26268e | |||
| 2cf7b28793 | |||
| d24d59c2ba | |||
| f190a33e21 | |||
| eb636ef731 | |||
| 2ae3aade60 | |||
| f227fe9310 | |||
| 7940d02bc7 | |||
| 2483ef2840 | |||
| 68e9693f9a | |||
| c9bbe1ffd8 | |||
| 91a3fe140d | |||
| c2c17741e5 | |||
| cac88d1560 | |||
| 317575644f | |||
| a756f5e4b7 | |||
| e4575a6b1e | |||
| 3ec3ab30fe | |||
| 7fb3ecff48 | |||
| ec59dad025 | |||
| 28b7b1620c | |||
| 9f8e240a38 | |||
| e6c05ab494 | |||
| 8aa9cf7119 | |||
| 9683e3799f | |||
| 200d293750 | |||
| ed196caebf | |||
| a049796b81 | |||
| 384fc9dd40 | |||
| 5f2df57e64 | |||
| 130d8a9edc | |||
| 40ae3d825a | |||
| 484624104e | |||
| e7c4bdb150 | |||
| 7210366e7d | |||
| 435bf27176 | |||
| 1ba27ac7cf | |||
| e4ddec3a02 | |||
| bc7b5c97d1 | |||
| fa720f8e6b | |||
| eff0f66d95 | |||
| b43c252224 | |||
| aae7aa981d | |||
| 6f9a7301af | |||
| 48923d03d4 | |||
| 019a83cf11 | |||
| 140d9e6e35 | |||
| a351b0f1b5 | |||
| 3cb73ff0c3 | |||
| 3b08c2bede | |||
| 86378d6cc4 | |||
| 00d1c583af | |||
| cfc246c80d | |||
| f3c80f9e5f | |||
| 0d3a5748dd | |||
| 079431754c | |||
| 46f5eca33d | |||
| 7081e939fb | |||
| 1e29460d6f | |||
| e180a89644 | |||
| 34a267e982 | |||
| c81aad7c67 | |||
| 2e677c2c7b | |||
| 4ea7b22b44 | |||
| b8b937bfca | |||
| 6cc29c4fa1 | |||
| 5451ba646d | |||
| 7720437ca5 | |||
| eb0a7a917d | |||
| 6f65903552 | |||
| 97e74d09be | |||
| 9c7560ed2e | |||
| 2979ceedcf | |||
| 745f59278f | |||
| 49c59b32ca | |||
| ff78140a7d | |||
| 82ff71b753 | |||
| f46e7cd846 | |||
| 9d73eb67c4 | |||
| 21ce041353 | |||
| 7529820c0b | |||
| 9febadffd3 | |||
| 99aacca94b | |||
| 1d517d4e09 | |||
| 047f361ea9 | |||
| 489244a015 | |||
| 8bab07ed72 | |||
| 1253f51ff3 | |||
| 23afb0bd33 | |||
| c596213b2a | |||
| 054a834464 | |||
| d7bfe86524 | |||
| 84ed27f222 | |||
| 6008d9841f | |||
| 6402cf7be5 | |||
| 9a96e1247b | |||
| 989134f4be | |||
| 120aec08da | |||
| e66ae7cc89 | |||
| b95fbd0ed0 | |||
| 32dc8e6636 | |||
| 8e3942d38c | |||
| d84ce0c6fa | |||
| 8d07a4cd05 | |||
| 8485460374 | |||
| 9fdc6fa3ed | |||
| 17004d58df | |||
| 6362a5e665 | |||
| d38d73d5c6 | |||
| 0a6571726a | |||
| e62557bd1d | |||
| ee90ee9392 | |||
| 5f9eaff59c | |||
| b86341ce7a | |||
| 4857739eec | |||
| 3bb4b099c1 | |||
| e7912a088f | |||
| 95d63d969e | |||
| 1f96bab944 | |||
| f98491f3bd | |||
| 98f262ae22 | |||
| d2ff53052c | |||
| ecac24c1d2 | |||
| a764b095f8 | |||
| 95a196a91f | |||
| 6b59980874 | |||
| 0c977514af | |||
| 1207730ce3 | |||
| 0d9dcd122c | |||
| 8a69e05ee2 | |||
| 976af290af | |||
| a3443ab1d5 | |||
| a27360482b | |||
| 3f9604a370 | |||
| 480afd1341 | |||
| ab71275566 | |||
| 2d850f457f | |||
| c423cc647d | |||
| 9e1142bf05 | |||
| 1843ca6c53 | |||
| caa5d92c32 | |||
| f41693bc6d | |||
| b7092fa362 | |||
| 0e7dcafbab | |||
| a574527891 | |||
| 176673017c | |||
| 1d6226d829 | |||
| 12b712c217 | |||
| 2de280915a | |||
| 1cce49f5e0 | |||
| 682a7e6566 | |||
| fb63dbd698 | |||
| 4f433d00cc | |||
| 6cf5115ea9 | |||
| f11a43010d | |||
| d1055e4d41 | |||
| 8554688230 | |||
| 3e873f378e | |||
| 28ce14ce34 | |||
| 5066cd4cfe | |||
| 0bfb3855b6 | |||
| 2f0dd20f1e | |||
| abef68c274 | |||
| 9aff614ff5 | |||
| 7b0e8a2fca | |||
| 3e68d6df4f | |||
| b75dc82f90 | |||
| f53ca3bd5b | |||
| 02885af1ca | |||
| e6e2a69506 | |||
| e4e92710c0 | |||
| f08bc9976c | |||
| 23183da2e1 | |||
| c6fef1693e | |||
| 192e03aa98 | |||
| 6f02b61527 | |||
| a21ff5633c | |||
| f96a6b94dc | |||
| e9f3aa25d2 | |||
| d0a8e96b70 | |||
| b09dc815fc | |||
| ceaac78633 | |||
| dc7a127fa6 | |||
| 3abe07c5b2 | |||
| 01bd7604ed | |||
| 552cd352f2 | |||
| c7f2955ee9 | |||
| ef36ea1e03 | |||
| d341cb24c0 | |||
| 2fabb67942 | |||
| a0b0ad370e | |||
| 7ae84a0d5a | |||
| 283b947fc5 | |||
| df3f00261a | |||
| bf78ac21fe | |||
| ab610147a5 | |||
| 7720fe9f9f | |||
| 7aeac86bd3 | |||
| ac49cd8b1c | |||
| af44bd063c | |||
| 1239d1c35f | |||
| f41a9ccf26 | |||
| ab1c4223d5 | |||
| c3a512d5cf | |||
| 4a60c42cd0 | |||
| be05b5d102 | |||
| 3f061750cf | |||
| 6d5d6345e2 | |||
| 6fea580693 | |||
| b35134625b | |||
| c3db609a90 | |||
| cc626c38f7 | |||
| a8b3f4f86c | |||
| d593969408 | |||
| 6d5895ebc2 | |||
| c9ee6e4f17 | |||
| a622c53e0f | |||
| a4f1363aed | |||
| 3a819dcaee | |||
| 729270b34b | |||
| 44cbcccb6c | |||
| 86b9ec56d7 | |||
| 253944afd2 | |||
| 54993ce5c2 | |||
| 05083bd513 | |||
| 6e4c340780 | |||
| 9dbca410c2 | |||
| 62ca3b5ffe | |||
| f263c35156 | |||
| 0678d70309 | |||
| 96fa5687f8 | |||
| 4d0dd68d56 | |||
| 89b0a07419 | |||
| 469ca43eaa | |||
| dc2b611cbc | |||
| 0c1acf1420 | |||
| 71b97e3e20 | |||
| 12ba3605e9 | |||
| d7427f306f | |||
| 0142381ce2 | |||
| 9223d7b5c5 | |||
| 3b74ade455 | |||
| dadcb69f1c | |||
| 2fd2a46838 | |||
| 1a66887f48 | |||
| 23f3cbcea7 | |||
| 429f51437f | |||
| c92272dd35 | |||
| 8792740eb0 | |||
| cf5faaad7f | |||
| 59b3d6514b | |||
| 3c956e671a | |||
| 8650297866 | |||
| 6831f11179 | |||
| d4e8e1bf73 | |||
| 08f2b283c9 | |||
| c38ecf6623 | |||
| 81807ace34 | |||
| 2f02f94b61 | |||
| 690ff7ffbc | |||
| bda88d3f8e | |||
| ba3e02052d | |||
| 9099b3eaec | |||
| 03da7c5cfc | |||
| cecf45061c | |||
| da9df2e6bf | |||
| 929eef31ef | |||
| 28cc0c5eec | |||
| 99d6c193f6 | |||
| 4f33db206c | |||
| 6ccbab402f | |||
| 7b01f1dde3 | |||
| 8ceb1d1ac3 | |||
| 668343532f | |||
| 84ad58b775 | |||
| 17f60d721b | |||
| d18c69fae1 | |||
| 9c58116c82 | |||
| 18f164bdec | |||
| 8d9ff2df3b | |||
| ffcd2de44d | |||
| 8dd2746411 | |||
| 7f6c318069 | |||
| d2e0f17ca8 | |||
| 4af6c4dcad | |||
| 5c8b132cb9 | |||
| 244ea143d4 | |||
| 58c372bcb3 | |||
| 168ab29be3 | |||
| 61f6743f0a | |||
| 6cd07498fe | |||
| 83e4094ca9 | |||
| 5654ee91da | |||
| c27da22bcb | |||
| b095828ae2 | |||
| b31d6ff144 | |||
| 8d5067996f | |||
| 1ba2055450 | |||
| 8658143b16 | |||
| 475e07c4e2 | |||
| 1847d96219 | |||
| 7e45864552 | |||
| 430617de7e | |||
| fa2ff0a242 | |||
| 7d91f88c4d | |||
| c1466a5bca | |||
| 44b95b69ca | |||
| 0e1cbb30b7 | |||
| f489129949 | |||
| 0d0a318e46 | |||
| 18811933bf | |||
| 912d0c8eac | |||
| b88888fc68 | |||
| e80486975f | |||
| 63944c5f93 | |||
| ce9be8750d | |||
| 6afcffb4ed | |||
| af004cb864 | |||
| 980f2af414 | |||
| 87908e8713 | |||
| 0f3cc434a3 | |||
| d943b78a25 | |||
| 744aa3fbdf | |||
| 9fa362ec6a | |||
| ca51b7ce01 | |||
| 2e005f6eb5 | |||
| cbc6b93cd8 | |||
| 9ae1b6ce15 | |||
| 1a1f2cf634 | |||
| 0d691865bc | |||
| 0fb1cafd20 | |||
| 1adf7d7fcc | |||
| 3ded17bf8b | |||
| 715442d7bf | |||
| e464a3fdd5 | |||
| fed4c179e6 | |||
| 32c22c3148 | |||
| 4557b094e1 | |||
| 84500305db | |||
| 0d21f84529 | |||
| 5bcc02a931 | |||
| fe91a176e2 | |||
| 083ee21e38 | |||
| ea5a1ab2de | |||
| de5cc438ab |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
tests/c-form/vmlinux.h linguist-vendored
|
||||
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Maintain dependencies for GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
actions:
|
||||
patterns:
|
||||
- "*"
|
||||
19
.github/workflows/format.yml
vendored
Normal file
19
.github/workflows/format.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
# This is a format job. Pre-commit has a first-party GitHub action, so we use
|
||||
# that: https://github.com/pre-commit/action
|
||||
|
||||
name: Format
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
name: Format
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
6
.github/workflows/python-publish.yml
vendored
6
.github/workflows/python-publish.yml
vendored
@ -20,9 +20,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
@ -59,7 +59,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Retrieve release distributions
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: release-dists
|
||||
path: dist/
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -5,4 +5,8 @@
|
||||
.vscode/
|
||||
__pycache__/
|
||||
*.ll
|
||||
*.o
|
||||
*.o
|
||||
.ipynb_checkpoints/
|
||||
vmlinux.py
|
||||
~*
|
||||
vmlinux.h
|
||||
|
||||
59
.pre-commit-config.yaml
Normal file
59
.pre-commit-config.yaml
Normal file
@ -0,0 +1,59 @@
|
||||
# To use:
|
||||
#
|
||||
# pre-commit run -a
|
||||
#
|
||||
# Or:
|
||||
#
|
||||
# pre-commit install # (runs every time you commit in git)
|
||||
#
|
||||
# To update this file:
|
||||
#
|
||||
# pre-commit autoupdate
|
||||
#
|
||||
# See https://github.com/pre-commit/pre-commit
|
||||
|
||||
exclude: 'vmlinux.py'
|
||||
|
||||
ci:
|
||||
autoupdate_commit_msg: "chore: update pre-commit hooks"
|
||||
autofix_commit_msg: "style: pre-commit fixes"
|
||||
|
||||
repos:
|
||||
# Standard hooks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-case-conflict
|
||||
- id: check-merge-conflict
|
||||
- id: check-symlinks
|
||||
- id: check-yaml
|
||||
exclude: ^conda\.recipe/meta\.yaml$
|
||||
- id: debug-statements
|
||||
- id: end-of-file-fixer
|
||||
- id: mixed-line-ending
|
||||
- id: requirements-txt-fixer
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: "v0.13.2"
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: ["--fix", "--show-fixes"]
|
||||
- id: ruff-format
|
||||
# exclude: ^(docs)|^(tests)|^(examples)
|
||||
|
||||
# Checking static types
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: "v1.18.2"
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: ^(tests)|^(examples)
|
||||
additional_dependencies: [types-setuptools]
|
||||
|
||||
# Changes tabs to spaces
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: v1.5.5
|
||||
hooks:
|
||||
- id: remove-tabs
|
||||
exclude: '^(docs)|.*/Makefile$|Makefile$'
|
||||
1
LICENSE
1
LICENSE
@ -200,4 +200,3 @@
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
8
Makefile
8
Makefile
@ -1,14 +1,10 @@
|
||||
compile:
|
||||
chmod +x ./tools/compile.py
|
||||
./tools/compile.py ./examples/execve3.py
|
||||
|
||||
install:
|
||||
install:
|
||||
pip install -e .
|
||||
|
||||
clean:
|
||||
rm -rf build dist *.egg-info
|
||||
rm -rf examples/*.ll examples/*.o
|
||||
|
||||
all: install compile
|
||||
all: clean install
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
220
README.md
220
README.md
@ -1,77 +1,203 @@
|
||||
# Python-BPF
|
||||
<picture>
|
||||
<source
|
||||
media="(prefers-color-scheme: light)"
|
||||
srcset="https://github.com/user-attachments/assets/f3738131-d7cb-4b5c-8699-c7010295a159"
|
||||
width="450"
|
||||
alt="Light‐mode image">
|
||||
<img
|
||||
src="https://github.com/user-attachments/assets/b175bf39-23cb-475d-a6e1-7b5c99a1ed72"
|
||||
width="450"
|
||||
alt="Dark‐mode image">
|
||||
</picture>
|
||||
<!-- Badges -->
|
||||
<p align="center">
|
||||
<a href="https://www.python.org/downloads/release/python-3080/"><img src="https://img.shields.io/badge/python-3.8-blue.svg"></a>
|
||||
<a href="https://pypi.org/project/pythonbpf"><img src="https://badge.fury.io/py/pythonbpf.svg"></a>
|
||||
<!-- PyPI -->
|
||||
<a href="https://pypi.org/project/pythonbpf/"><img src="https://img.shields.io/pypi/v/pythonbpf?color=blue" alt="PyPI version"></a>
|
||||
<!-- <a href="https://pypi.org/project/pythonbpf/"><img src="https://img.shields.io/pypi/pyversions/pythonbpf" alt="Python versions"></a> -->
|
||||
<!-- <a href="https://pypi.org/project/pythonbpf/"><img src="https://img.shields.io/pypi/dm/pythonbpf" alt="PyPI downloads"></a> -->
|
||||
<!-- <a href="https://pypi.org/project/pythonbpf/"><img src="https://img.shields.io/pypi/status/pythonbpf" alt="PyPI Status"></a> -->
|
||||
<a href="https://pepy.tech/project/pythonbpf"><img src="https://pepy.tech/badge/pythonbpf" alt="Downloads"></a>
|
||||
<!-- Build & CI -->
|
||||
<a href="https://github.com/pythonbpf/python-bpf/actions"><img src="https://github.com/pythonbpf/python-bpf/actions/workflows/python-publish.yml/badge.svg" alt="Build Status"></a>
|
||||
<!-- Meta -->
|
||||
<a href="https://github.com/pythonbpf/python-bpf/blob/main/LICENSE"><img src="https://img.shields.io/github/license/pythonbpf/python-bpf" alt="License"></a>
|
||||
</p>
|
||||
|
||||
This is an LLVM IR generator for eBPF programs in Python. We use llvmlite to generate LLVM IR from pure Python. This is then compiled to LLVM object files, which can be loaded into the kernel for execution. We do not rely on BCC to do our compilation.
|
||||
|
||||
# DO NOT USE IN PRODUCTION. IN DEVELOPMENT.
|
||||
Python-BPF is an LLVM IR generator for eBPF programs written in Python. It uses [llvmlite](https://github.com/numba/llvmlite) to generate LLVM IR and then compiles to LLVM object files. These object files can be loaded into the kernel for execution. Python-BPF performs compilation without relying on BCC.
|
||||
|
||||
## Video Demo
|
||||
[Video demo for code under demo/](https://youtu.be/eMyLW8iWbks)
|
||||
> **Note**: This project is under active development and not ready for production use.
|
||||
|
||||
## Slide Deck
|
||||
[Slide deck explaining the project](https://docs.google.com/presentation/d/1DsWDIVrpJhM4RgOETO9VWqUtEHo3-c7XIWmNpi6sTSo/edit?usp=sharing)
|
||||
---
|
||||
|
||||
## Installation
|
||||
- Have `clang` installed.
|
||||
- `pip install pythonbpf`
|
||||
## Overview
|
||||
|
||||
* Generate eBPF programs directly from Python.
|
||||
* Compile to LLVM object files for kernel execution.
|
||||
* Built with `llvmlite` for IR generation.
|
||||
* Supports maps, helpers, and global definitions for BPF.
|
||||
* Companion project: [pylibbpf](https://github.com/pythonbpf/pylibbpf), which provides the bindings required for object loading and execution.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Dependencies:
|
||||
|
||||
* `clang`
|
||||
* Python ≥ 3.8
|
||||
|
||||
Install via pip:
|
||||
|
||||
```bash
|
||||
pip install pythonbpf pylibbpf
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example Usage
|
||||
|
||||
## Usage
|
||||
```python
|
||||
# pythonbpf_example.py
|
||||
from pythonbpf import bpf, map, bpfglobal, section, compile
|
||||
from pythonbpf.helpers import bpf_ktime_get_ns
|
||||
import time
|
||||
from pythonbpf import bpf, map, section, bpfglobal, BPF
|
||||
from pythonbpf.helper import pid
|
||||
from pythonbpf.maps import HashMap
|
||||
from pylibbpf import *
|
||||
from ctypes import c_void_p, c_int64, c_uint64, c_int32
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from ctypes import c_void_p, c_int64, c_int32, c_uint64
|
||||
|
||||
# This program attaches an eBPF tracepoint to sys_enter_clone,
|
||||
# counts per-PID clone syscalls, stores them in a hash map,
|
||||
# and then plots the distribution as a histogram using matplotlib.
|
||||
# It provides a quick view of process creation activity over 10 seconds.
|
||||
|
||||
@bpf
|
||||
@map
|
||||
def last() -> HashMap:
|
||||
return HashMap(key=c_uint64, value=c_uint64, max_entries=1)
|
||||
def hist() -> HashMap:
|
||||
return HashMap(key=c_int32, value=c_uint64, max_entries=4096)
|
||||
|
||||
|
||||
@bpf
|
||||
@section("tracepoint/syscalls/sys_enter_execve")
|
||||
def hello(ctx: c_void_p) -> c_int32:
|
||||
print("entered")
|
||||
return c_int32(0)
|
||||
|
||||
@bpf
|
||||
@section("tracepoint/syscalls/sys_exit_execve")
|
||||
def hello_again(ctx: c_void_p) -> c_int64:
|
||||
print("exited")
|
||||
key = 0
|
||||
tsp = last().lookup(key)
|
||||
print(tsp)
|
||||
ts = bpf_ktime_get_ns()
|
||||
@section("tracepoint/syscalls/sys_enter_clone")
|
||||
def hello(ctx: c_void_p) -> c_int64:
|
||||
process_id = pid()
|
||||
one = 1
|
||||
prev = hist.lookup(process_id)
|
||||
if prev:
|
||||
previous_value = prev + 1
|
||||
print(f"count: {previous_value} with {process_id}")
|
||||
hist.update(process_id, previous_value)
|
||||
return c_int64(0)
|
||||
else:
|
||||
hist.update(process_id, one)
|
||||
return c_int64(0)
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def LICENSE() -> str:
|
||||
return "GPL"
|
||||
|
||||
def some_normal_function():
|
||||
print("normal function")
|
||||
|
||||
# compiles and dumps object file in the same directory
|
||||
compile()
|
||||
b = BPF()
|
||||
b.load_and_attach()
|
||||
hist = BpfMap(b, hist)
|
||||
print("Recording")
|
||||
time.sleep(10)
|
||||
|
||||
counts = list(hist.values())
|
||||
|
||||
plt.hist(counts, bins=20)
|
||||
plt.xlabel("Clone calls per PID")
|
||||
plt.ylabel("Frequency")
|
||||
plt.title("Syscall clone counts")
|
||||
plt.show()
|
||||
```
|
||||
- Run `python pythonbpf_example.py` to get the compiled object file that can be then loaded into the kernel.
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
Python-BPF provides a complete pipeline to write, compile, and load eBPF programs in Python:
|
||||
|
||||
1. **Python Source Code**
|
||||
|
||||
* Users write BPF programs in Python using decorators like `@bpf`, `@map`, `@section`, and `@bpfglobal`.
|
||||
* Maps (hash maps), helpers (e.g., `ktime`, `deref`), and tracepoints are defined using Python constructs, preserving a syntax close to standard Python.
|
||||
|
||||
2. **AST Generation**
|
||||
|
||||
* The Python `ast` module parses the source code into an Abstract Syntax Tree (AST).
|
||||
* Decorators and type annotations are captured to determine BPF maps, tracepoints, and global variables.
|
||||
|
||||
3. **LLVM IR Emission**
|
||||
|
||||
* The AST is transformed into LLVM Intermediate Representation (IR) using `llvmlite`.
|
||||
* IR captures BPF maps, control flow, assignments, and calls to helper functions.
|
||||
* Debug information is emitted for easier inspection.
|
||||
|
||||
4. **LLVM Object File Compilation**
|
||||
|
||||
* The LLVM IR (`.ll`) is compiled into a BPF target object file (`.o`) using `llc -march=bpf -O2`.
|
||||
* This produces a kernel-loadable ELF object file containing the BPF bytecode.
|
||||
|
||||
5. **libbpf Integration (via pylibbpf)**
|
||||
|
||||
* The compiled object file can be loaded into the kernel using `pylibbpf`.
|
||||
* Maps, tracepoints, and program sections are initialized, and helper functions are resolved.
|
||||
* Programs are attached to kernel hooks (e.g., syscalls) for execution.
|
||||
|
||||
6. **Execution in Kernel**
|
||||
|
||||
* The kernel executes the loaded eBPF program.
|
||||
* Hash maps, helpers, and global variables behave as defined in the Python source.
|
||||
* Output can be read via BPF maps, helper functions, or trace printing.
|
||||
|
||||
This architecture eliminates the need for embedding C code in Python, allowing full Python tooling support while generating true BPF object files ready for kernel execution.
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
- Make a virtual environment and activate it using `python3 -m venv .venv && source .venv/bin/activate`.
|
||||
- Run `make install` to install the required dependencies.
|
||||
- Run `make` to see the compilation output of the example.
|
||||
- Run `check.sh` to check if generated object file passes through the verifier inside the examples directory.
|
||||
- Run `make` in the `examples/c-form` directory to modify the example C BPF program to check the actual LLVM IR generated by clang.
|
||||
|
||||
### Development Notes
|
||||
- Run ` ./check.sh check execve2.o;` in examples folder to check if the object code passes the verifier.
|
||||
- Run ` ./check.sh run execve2.o;` in examples folder to run the object code using `bpftool`.
|
||||
1. Create a virtual environment and activate it:
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
make install
|
||||
```
|
||||
Then, run any example in `examples`
|
||||
3. Verify an object file with the kernel verifier:
|
||||
|
||||
```bash
|
||||
./tools/check.sh check execve2.o
|
||||
```
|
||||
|
||||
5. Run an object file using `bpftool`:
|
||||
|
||||
```bash
|
||||
./tools/check.sh run execve2.o
|
||||
```
|
||||
|
||||
6. Explore LLVM IR output from clang in `examples/c-form` by running `make`.
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
* [Video demonstration](https://youtu.be/eMyLW8iWbks)
|
||||
* [Slide deck](https://docs.google.com/presentation/d/1DsWDIVrpJhM4RgOETO9VWqUtEHo3-c7XIWmNpi6sTSo/edit?usp=sharing)
|
||||
|
||||
---
|
||||
|
||||
## Authors
|
||||
- [@r41k0u](https://github.com/r41k0u)
|
||||
- [@varun-r-mallya](https://github.com/varun-r-mallya)
|
||||
|
||||
* [@r41k0u](https://github.com/r41k0u)
|
||||
* [@varun-r-mallya](https://github.com/varun-r-mallya)
|
||||
|
||||
---
|
||||
|
||||
9
TODO.md
9
TODO.md
@ -1,9 +0,0 @@
|
||||
## Short term
|
||||
|
||||
- Implement enough functionality to port the BCC tutorial examples in PythonBPF
|
||||
|
||||
|
||||
## Long term
|
||||
|
||||
- Refactor the codebase to be better than a hackathon project
|
||||
- Port to C++ and use actual LLVM?
|
||||
46
demo/bcc.py
46
demo/bcc.py
@ -1,46 +0,0 @@
|
||||
from __future__ import print_function
|
||||
from bcc import BPF
|
||||
from bcc.utils import printb
|
||||
|
||||
# load BPF program
|
||||
b = BPF(text="""
|
||||
#include <uapi/linux/ptrace.h>
|
||||
|
||||
BPF_HASH(last);
|
||||
|
||||
int do_trace(struct pt_regs *ctx) {
|
||||
u64 ts, *tsp, delta, key = 0;
|
||||
|
||||
// attempt to read stored timestamp
|
||||
tsp = last.lookup(&key);
|
||||
if (tsp != NULL) {
|
||||
delta = bpf_ktime_get_ns() - *tsp;
|
||||
if (delta < 1000000000) {
|
||||
// output if time is less than 1 second
|
||||
bpf_trace_printk("%d\\n", delta / 1000000);
|
||||
}
|
||||
last.delete(&key);
|
||||
}
|
||||
|
||||
// update stored timestamp
|
||||
ts = bpf_ktime_get_ns();
|
||||
last.update(&key, &ts);
|
||||
return 0;
|
||||
}
|
||||
""")
|
||||
|
||||
b.attach_kprobe(event=b.get_syscall_fnname("sync"), fn_name="do_trace")
|
||||
print("Tracing for quick sync's... Ctrl-C to end")
|
||||
|
||||
# TODO
|
||||
# format output
|
||||
start = 0
|
||||
while 1:
|
||||
try:
|
||||
(task, pid, cpu, flags, ts, ms) = b.trace_fields()
|
||||
if start == 0:
|
||||
start = ts
|
||||
ts = ts - start
|
||||
printb(b"At time %.2f s: multiple syncs detected, last %s ms ago" % (ts, ms))
|
||||
except KeyboardInterrupt:
|
||||
exit()
|
||||
@ -1,15 +1,16 @@
|
||||
from pythonbpf import bpf, map, section, bpfglobal, compile
|
||||
from pythonbpf.helpers import ktime
|
||||
from pythonbpf.helper import ktime
|
||||
from pythonbpf.maps import HashMap
|
||||
|
||||
from ctypes import c_void_p, c_int64, c_uint64
|
||||
|
||||
# Instructions to how to run this program
|
||||
# 1. Install PythonBPF: pip install pythonbpf
|
||||
# 2. Run the program: python demo/pybpf3.py
|
||||
# 3. Run the program with sudo: sudo examples/check.sh run demo/pybpf3.o
|
||||
# 2. Run the program: python examples/binops_demo.py
|
||||
# 3. Run the program with sudo: sudo tools/check.sh run examples/binops_demo.py
|
||||
# 4. Start up any program and watch the output
|
||||
|
||||
|
||||
@bpf
|
||||
@map
|
||||
def last() -> HashMap:
|
||||
@ -23,9 +24,9 @@ def do_trace(ctx: c_void_p) -> c_int64:
|
||||
tsp = last().lookup(key)
|
||||
if tsp:
|
||||
kt = ktime()
|
||||
delta = (kt - tsp)
|
||||
delta = kt - tsp
|
||||
if delta < 1000000000:
|
||||
time_ms = (delta // 1000000)
|
||||
time_ms = delta // 1000000
|
||||
print(f"Execve syscall entered within last second, last {time_ms} ms ago")
|
||||
last().delete(key)
|
||||
else:
|
||||
@ -33,16 +34,18 @@ def do_trace(ctx: c_void_p) -> c_int64:
|
||||
last().update(key, kt)
|
||||
return c_int64(0)
|
||||
|
||||
|
||||
@bpf
|
||||
@section("tracepoint/syscalls/sys_exit_execve")
|
||||
def do_exit(ctx: c_void_p) -> c_int64:
|
||||
va = 8
|
||||
nm = 5 ^ va
|
||||
al = 6 & 3
|
||||
ru = (nm + al)
|
||||
ru = nm + al
|
||||
print(f"this is a variable {ru}")
|
||||
return c_int64(0)
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def LICENSE() -> str:
|
||||
@ -1,8 +1,8 @@
|
||||
from pythonbpf import bpf, map, section, bpfglobal, compile
|
||||
from pythonbpf.helpers import ktime, deref
|
||||
from pythonbpf.helper import ktime
|
||||
from pythonbpf.maps import HashMap
|
||||
|
||||
from ctypes import c_void_p, c_int64, c_int32, c_uint64
|
||||
from ctypes import c_void_p, c_int32, c_uint64
|
||||
|
||||
|
||||
@bpf
|
||||
@ -15,7 +15,7 @@ def last() -> HashMap:
|
||||
@section("blk_start_request")
|
||||
def trace_start(ctx: c_void_p) -> c_int32:
|
||||
ts = ktime()
|
||||
print("req started")
|
||||
print(f"req started {ts}")
|
||||
return c_int32(0)
|
||||
|
||||
|
||||
121617
examples/c-form/vmlinux.h
121617
examples/c-form/vmlinux.h
File diff suppressed because it is too large
Load Diff
398
examples/clone-matplotlib.ipynb
Normal file
398
examples/clone-matplotlib.ipynb
Normal file
File diff suppressed because one or more lines are too long
@ -1,9 +1,9 @@
|
||||
import time
|
||||
|
||||
from pythonbpf import bpf, map, section, bpfglobal, BPF
|
||||
from pythonbpf.helpers import pid
|
||||
from pythonbpf.helper import pid
|
||||
from pythonbpf.maps import HashMap
|
||||
from pylibbpf import *
|
||||
from pylibbpf import BpfMap
|
||||
from ctypes import c_void_p, c_int64, c_uint64, c_int32
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
@ -12,13 +12,15 @@ import matplotlib.pyplot as plt
|
||||
# and then plots the distribution as a histogram using matplotlib.
|
||||
# It provides a quick view of process creation activity over 10 seconds.
|
||||
# Everything is done with Python only code and with the new pylibbpf library.
|
||||
# Run `sudo /path/to/python/binary/ pybpf4.py`
|
||||
# Run `sudo /path/to/python/binary/ clone_plot.py`
|
||||
|
||||
|
||||
@bpf
|
||||
@map
|
||||
def hist() -> HashMap:
|
||||
return HashMap(key=c_int32, value=c_uint64, max_entries=4096)
|
||||
|
||||
|
||||
@bpf
|
||||
@section("tracepoint/syscalls/sys_enter_clone")
|
||||
def hello(ctx: c_void_p) -> c_int64:
|
||||
@ -1,35 +0,0 @@
|
||||
from pythonbpf.decorators import bpf, map, section, bpfglobal
|
||||
from ctypes import c_void_p, c_int64, c_int32, c_uint64
|
||||
from pythonbpf.helpers import ktime
|
||||
from pythonbpf.maps import HashMap
|
||||
|
||||
|
||||
@bpf
|
||||
@map
|
||||
def last() -> HashMap:
|
||||
return HashMap(key=c_uint64, value=c_uint64, max_entries=1)
|
||||
|
||||
|
||||
@bpf
|
||||
@section("tracepoint/syscalls/sys_enter_execve")
|
||||
def hello(ctx: c_void_p) -> c_int32:
|
||||
print("entered")
|
||||
print("multi constant support")
|
||||
return c_int32(0)
|
||||
|
||||
|
||||
@bpf
|
||||
@section("tracepoint/syscalls/sys_exit_execve")
|
||||
def hello_again(ctx: c_void_p) -> c_int64:
|
||||
print("exited")
|
||||
key = 0
|
||||
tsp = last().lookup(key)
|
||||
print(tsp)
|
||||
ts = ktime()
|
||||
return c_int64(0)
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def LICENSE() -> str:
|
||||
return "GPL"
|
||||
@ -1,55 +0,0 @@
|
||||
from pythonbpf import bpf, map, section, bpfglobal, compile
|
||||
from pythonbpf.helpers import ktime, deref
|
||||
from pythonbpf.maps import HashMap
|
||||
|
||||
from ctypes import c_void_p, c_int64, c_int32, c_uint64
|
||||
|
||||
|
||||
@bpf
|
||||
@map
|
||||
def last() -> HashMap:
|
||||
return HashMap(key=c_uint64, value=c_uint64, max_entries=3)
|
||||
|
||||
|
||||
@bpf
|
||||
@section("tracepoint/syscalls/sys_enter_execve")
|
||||
def hello(ctx: c_void_p) -> c_int32:
|
||||
print("entered")
|
||||
print("multi constant support")
|
||||
return c_int32(0)
|
||||
|
||||
|
||||
@bpf
|
||||
@section("tracepoint/syscalls/sys_exit_execve")
|
||||
def hello_again(ctx: c_void_p) -> c_int64:
|
||||
print("exited")
|
||||
key = 0
|
||||
delta = 0
|
||||
dddelta = 0
|
||||
tsp = last().lookup(key)
|
||||
if True:
|
||||
delta = ktime()
|
||||
ddelta = deref(delta)
|
||||
ttsp = deref(deref(tsp))
|
||||
dddelta = ddelta - ttsp
|
||||
if dddelta < 1000000000:
|
||||
print("execve called within last second")
|
||||
last().delete(key)
|
||||
ts = ktime()
|
||||
last().update(key, ts)
|
||||
|
||||
va = 8
|
||||
nm = 5 + va
|
||||
al = 6 & 3
|
||||
print(f"this is a variable {nm}")
|
||||
|
||||
return c_int64(0)
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def LICENSE() -> str:
|
||||
return "GPL"
|
||||
|
||||
|
||||
compile()
|
||||
@ -1,15 +1,30 @@
|
||||
# This is what it is going to look like
|
||||
# pylint: disable-all# type: ignore
|
||||
from pythonbpf.decorators import tracepoint, syscalls, bpfglobal, bpf
|
||||
from ctypes import c_void_p, c_int32
|
||||
from pythonbpf import bpf, section, bpfglobal, BPF
|
||||
from ctypes import c_void_p, c_int64
|
||||
|
||||
# Instructions to how to run this program
|
||||
# 1. Install PythonBPF: pip install pythonbpf
|
||||
# 2. Run the program: sudo python examples/hello_world.py
|
||||
# 4. Start up any program and watch the output
|
||||
|
||||
|
||||
@bpf
|
||||
@tracepoint(syscalls.sys_clone)
|
||||
def trace_clone(ctx: c_void_p) -> c_int32:
|
||||
@section("tracepoint/syscalls/sys_enter_execve")
|
||||
def hello_world(ctx: c_void_p) -> c_int64:
|
||||
print("Hello, World!")
|
||||
return c_int32(0)
|
||||
return c_int64(0)
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def LICENSE() -> str:
|
||||
return "GPL"
|
||||
|
||||
|
||||
b = BPF()
|
||||
b.load_and_attach()
|
||||
if b.is_loaded() and b.is_attached():
|
||||
print("Successfully loaded and attached")
|
||||
else:
|
||||
print("Could not load successfully")
|
||||
|
||||
# Now cat /sys/kernel/debug/tracing/trace_pipe to see results of the execve syscall.
|
||||
|
||||
29
examples/kprobes.py
Normal file
29
examples/kprobes.py
Normal file
@ -0,0 +1,29 @@
|
||||
from pythonbpf import bpf, section, bpfglobal, BPF
|
||||
from ctypes import c_void_p, c_int64
|
||||
|
||||
|
||||
@bpf
|
||||
@section("kretprobe/do_unlinkat")
|
||||
def hello_world(ctx: c_void_p) -> c_int64:
|
||||
print("Hello, World!")
|
||||
return c_int64(0)
|
||||
|
||||
|
||||
@bpf
|
||||
@section("kprobe/do_unlinkat")
|
||||
def hello_world2(ctx: c_void_p) -> c_int64:
|
||||
print("Hello, World!")
|
||||
return c_int64(0)
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def LICENSE() -> str:
|
||||
return "GPL"
|
||||
|
||||
|
||||
b = BPF()
|
||||
b.load_and_attach()
|
||||
while True:
|
||||
print("running")
|
||||
# Now cat /sys/kernel/debug/tracing/trace_pipe to see results of unlink kprobe.
|
||||
@ -1,8 +1,8 @@
|
||||
from pythonbpf import bpf, map, struct, section, bpfglobal, compile
|
||||
from pythonbpf.helpers import ktime, pid
|
||||
from pythonbpf.helper import ktime, pid
|
||||
from pythonbpf.maps import PerfEventArray
|
||||
|
||||
from ctypes import c_void_p, c_int64, c_int32, c_uint64
|
||||
from ctypes import c_void_p, c_int32, c_uint64
|
||||
|
||||
|
||||
@bpf
|
||||
@ -23,13 +23,11 @@ def events() -> PerfEventArray:
|
||||
@section("tracepoint/syscalls/sys_enter_clone")
|
||||
def hello(ctx: c_void_p) -> c_int32:
|
||||
dataobj = data_t()
|
||||
ts = ktime()
|
||||
process_id = pid()
|
||||
strobj = "hellohellohello"
|
||||
dataobj.pid = process_id
|
||||
dataobj.ts = ts
|
||||
dataobj.pid = pid()
|
||||
dataobj.ts = ktime()
|
||||
# dataobj.comm = strobj
|
||||
print(f"clone called at {ts} by pid {process_id}, comm {strobj}")
|
||||
print(f"clone called at {dataobj.ts} by pid{dataobj.pid}, comm {strobj}")
|
||||
events.output(dataobj)
|
||||
return c_int32(0)
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
from pythonbpf import bpf, map, section, bpfglobal, compile
|
||||
from pythonbpf.helpers import ktime
|
||||
from pythonbpf.helper import ktime
|
||||
from pythonbpf.maps import HashMap
|
||||
|
||||
from ctypes import c_void_p, c_int64, c_uint64
|
||||
|
||||
# Instructions to how to run this program
|
||||
# 1. Install PythonBPF: pip install pythonbpf
|
||||
# 2. Run the program: python demo/pybpf2.py
|
||||
# 3. Run the program with sudo: sudo examples/check.sh run demo/pybpf2.o
|
||||
# 2. Run the program: python examples/sys_sync.py
|
||||
# 3. Run the program with sudo: sudo tools/check.sh run examples/sys_sync.o
|
||||
# 4. Start a Python repl and `import os` and then keep entering `os.sync()` to see reponses.
|
||||
|
||||
|
||||
@bpf
|
||||
@map
|
||||
def last() -> HashMap:
|
||||
@ -20,17 +21,17 @@ def last() -> HashMap:
|
||||
@section("tracepoint/syscalls/sys_enter_sync")
|
||||
def do_trace(ctx: c_void_p) -> c_int64:
|
||||
key = 0
|
||||
tsp = last().lookup(key)
|
||||
tsp = last.lookup(key)
|
||||
if tsp:
|
||||
kt = ktime()
|
||||
delta = (kt - tsp)
|
||||
delta = kt - tsp
|
||||
if delta < 1000000000:
|
||||
time_ms = (delta // 1000000)
|
||||
time_ms = delta // 1000000
|
||||
print(f"sync called within last second, last {time_ms} ms ago")
|
||||
last().delete(key)
|
||||
last.delete(key)
|
||||
else:
|
||||
kt = ktime()
|
||||
last().update(key, kt)
|
||||
last.update(key, kt)
|
||||
return c_int64(0)
|
||||
|
||||
|
||||
203381
examples/vmlinux.py
203381
examples/vmlinux.py
File diff suppressed because it is too large
Load Diff
@ -1,16 +1,17 @@
|
||||
from pythonbpf import bpf, map, section, bpfglobal, compile
|
||||
from pythonbpf.helpers import XDP_PASS
|
||||
from pythonbpf import bpf, map, section, bpfglobal, compile, compile_to_ir
|
||||
from pythonbpf.helper import XDP_PASS
|
||||
from pythonbpf.maps import HashMap
|
||||
from ctypes import c_int64, c_void_p
|
||||
|
||||
from ctypes import c_void_p, c_int64
|
||||
|
||||
# Instructions to how to run this program
|
||||
# 1. Install PythonBPF: pip install pythonbpf
|
||||
# 2. Run the program: python demo/pybpf1.py
|
||||
# 3. Run the program with sudo: sudo examples/check.sh run demo/pybpf1.o
|
||||
# 4. Attach object file to any network device with something like ./check.sh xdp ../demo/pybpf1.o tailscale0
|
||||
# 2. Run the program: python examples/xdp_pass.py
|
||||
# 3. Run the program with sudo: sudo tools/check.sh run examples/xdp_pass.o
|
||||
# 4. Attach object file to any network device with something like ./check.sh xdp examples/xdp_pass.o tailscale0
|
||||
# 5. send traffic through the device and observe effects
|
||||
|
||||
|
||||
@bpf
|
||||
@map
|
||||
def count() -> HashMap:
|
||||
@ -33,9 +34,12 @@ def hello_world(ctx: c_void_p) -> c_int64:
|
||||
|
||||
return XDP_PASS
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def LICENSE() -> str:
|
||||
return "GPL"
|
||||
|
||||
|
||||
compile_to_ir("xdp_pass.py", "xdp_pass.ll")
|
||||
compile()
|
||||
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pythonbpf"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
description = "Reduced Python frontend for eBPF"
|
||||
authors = [
|
||||
{ name = "r41k0u", email="pragyanshchaturvedi18@gmail.com" },
|
||||
|
||||
@ -1,2 +1,13 @@
|
||||
from .decorators import bpf, map, section, bpfglobal, struct
|
||||
from .codegen import compile_to_ir, compile, BPF
|
||||
|
||||
__all__ = [
|
||||
"bpf",
|
||||
"map",
|
||||
"section",
|
||||
"bpfglobal",
|
||||
"struct",
|
||||
"compile_to_ir",
|
||||
"compile",
|
||||
"BPF",
|
||||
]
|
||||
|
||||
178
pythonbpf/allocation_pass.py
Normal file
178
pythonbpf/allocation_pass.py
Normal file
@ -0,0 +1,178 @@
|
||||
import ast
|
||||
import logging
|
||||
|
||||
from llvmlite import ir
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from pythonbpf.helper import HelperHandlerRegistry
|
||||
from pythonbpf.type_deducer import ctypes_to_ir
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LocalSymbol:
|
||||
var: ir.AllocaInstr
|
||||
ir_type: ir.Type
|
||||
metadata: Any = None
|
||||
|
||||
def __iter__(self):
|
||||
yield self.var
|
||||
yield self.ir_type
|
||||
yield self.metadata
|
||||
|
||||
|
||||
def handle_assign_allocation(builder, stmt, local_sym_tab, structs_sym_tab):
|
||||
"""Handle memory allocation for assignment statements."""
|
||||
|
||||
# Validate assignment
|
||||
if len(stmt.targets) != 1:
|
||||
logger.warning("Multi-target assignment not supported, skipping allocation")
|
||||
return
|
||||
|
||||
target = stmt.targets[0]
|
||||
|
||||
# Skip non-name targets (e.g., struct field assignments)
|
||||
if isinstance(target, ast.Attribute):
|
||||
logger.debug(f"Struct field assignment to {target.attr}, no allocation needed")
|
||||
return
|
||||
|
||||
if not isinstance(target, ast.Name):
|
||||
logger.warning(f"Unsupported assignment target type: {type(target).__name__}")
|
||||
return
|
||||
|
||||
var_name = target.id
|
||||
rval = stmt.value
|
||||
|
||||
# Skip if already allocated
|
||||
if var_name in local_sym_tab:
|
||||
logger.debug(f"Variable {var_name} already allocated, skipping")
|
||||
return
|
||||
|
||||
# Determine type and allocate based on rval
|
||||
if isinstance(rval, ast.Call):
|
||||
_allocate_for_call(builder, var_name, rval, local_sym_tab, structs_sym_tab)
|
||||
elif isinstance(rval, ast.Constant):
|
||||
_allocate_for_constant(builder, var_name, rval, local_sym_tab)
|
||||
elif isinstance(rval, ast.BinOp):
|
||||
_allocate_for_binop(builder, var_name, local_sym_tab)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Unsupported assignment value type for {var_name}: {type(rval).__name__}"
|
||||
)
|
||||
|
||||
|
||||
def _allocate_for_call(builder, var_name, rval, local_sym_tab, structs_sym_tab):
|
||||
"""Allocate memory for variable assigned from a call."""
|
||||
|
||||
if isinstance(rval.func, ast.Name):
|
||||
call_type = rval.func.id
|
||||
|
||||
# C type constructors
|
||||
if call_type in ("c_int32", "c_int64", "c_uint32", "c_uint64"):
|
||||
ir_type = ctypes_to_ir(call_type)
|
||||
var = builder.alloca(ir_type, name=var_name)
|
||||
var.align = ir_type.width // 8
|
||||
local_sym_tab[var_name] = LocalSymbol(var, ir_type)
|
||||
logger.info(f"Pre-allocated {var_name} as {call_type}")
|
||||
|
||||
# Helper functions
|
||||
elif HelperHandlerRegistry.has_handler(call_type):
|
||||
ir_type = ir.IntType(64) # Assume i64 return type
|
||||
var = builder.alloca(ir_type, name=var_name)
|
||||
var.align = 8
|
||||
local_sym_tab[var_name] = LocalSymbol(var, ir_type)
|
||||
logger.info(f"Pre-allocated {var_name} for helper {call_type}")
|
||||
|
||||
# Deref function
|
||||
elif call_type == "deref":
|
||||
ir_type = ir.IntType(64) # Assume i64 return type
|
||||
var = builder.alloca(ir_type, name=var_name)
|
||||
var.align = 8
|
||||
local_sym_tab[var_name] = LocalSymbol(var, ir_type)
|
||||
logger.info(f"Pre-allocated {var_name} for deref")
|
||||
|
||||
# Struct constructors
|
||||
elif call_type in structs_sym_tab:
|
||||
struct_info = structs_sym_tab[call_type]
|
||||
var = builder.alloca(struct_info.ir_type, name=var_name)
|
||||
local_sym_tab[var_name] = LocalSymbol(var, struct_info.ir_type, call_type)
|
||||
logger.info(f"Pre-allocated {var_name} for struct {call_type}")
|
||||
|
||||
else:
|
||||
logger.warning(f"Unknown call type for allocation: {call_type}")
|
||||
|
||||
elif isinstance(rval.func, ast.Attribute):
|
||||
# Map method calls - need double allocation for ptr handling
|
||||
_allocate_for_map_method(builder, var_name, local_sym_tab)
|
||||
|
||||
else:
|
||||
logger.warning(f"Unsupported call function type for {var_name}")
|
||||
|
||||
|
||||
def _allocate_for_map_method(builder, var_name, local_sym_tab):
|
||||
"""Allocate memory for variable assigned from map method (double alloc)."""
|
||||
|
||||
# Main variable (pointer to pointer)
|
||||
ir_type = ir.PointerType(ir.IntType(64))
|
||||
var = builder.alloca(ir_type, name=var_name)
|
||||
local_sym_tab[var_name] = LocalSymbol(var, ir_type)
|
||||
|
||||
# Temporary variable for computed values
|
||||
tmp_ir_type = ir.IntType(64)
|
||||
var_tmp = builder.alloca(tmp_ir_type, name=f"{var_name}_tmp")
|
||||
local_sym_tab[f"{var_name}_tmp"] = LocalSymbol(var_tmp, tmp_ir_type)
|
||||
|
||||
logger.info(f"Pre-allocated {var_name} and {var_name}_tmp for map method")
|
||||
|
||||
|
||||
def _allocate_for_constant(builder, var_name, rval, local_sym_tab):
|
||||
"""Allocate memory for variable assigned from a constant."""
|
||||
|
||||
if isinstance(rval.value, bool):
|
||||
ir_type = ir.IntType(1)
|
||||
var = builder.alloca(ir_type, name=var_name)
|
||||
var.align = 1
|
||||
local_sym_tab[var_name] = LocalSymbol(var, ir_type)
|
||||
logger.info(f"Pre-allocated {var_name} as bool")
|
||||
|
||||
elif isinstance(rval.value, int):
|
||||
ir_type = ir.IntType(64)
|
||||
var = builder.alloca(ir_type, name=var_name)
|
||||
var.align = 8
|
||||
local_sym_tab[var_name] = LocalSymbol(var, ir_type)
|
||||
logger.info(f"Pre-allocated {var_name} as i64")
|
||||
|
||||
elif isinstance(rval.value, str):
|
||||
ir_type = ir.PointerType(ir.IntType(8))
|
||||
var = builder.alloca(ir_type, name=var_name)
|
||||
var.align = 8
|
||||
local_sym_tab[var_name] = LocalSymbol(var, ir_type)
|
||||
logger.info(f"Pre-allocated {var_name} as string")
|
||||
|
||||
else:
|
||||
logger.warning(
|
||||
f"Unsupported constant type for {var_name}: {type(rval.value).__name__}"
|
||||
)
|
||||
|
||||
|
||||
def _allocate_for_binop(builder, var_name, local_sym_tab):
|
||||
"""Allocate memory for variable assigned from a binary operation."""
|
||||
ir_type = ir.IntType(64) # Assume i64 result
|
||||
var = builder.alloca(ir_type, name=var_name)
|
||||
var.align = 8
|
||||
local_sym_tab[var_name] = LocalSymbol(var, ir_type)
|
||||
logger.info(f"Pre-allocated {var_name} for binop result")
|
||||
|
||||
|
||||
def allocate_temp_pool(builder, max_temps, local_sym_tab):
|
||||
"""Allocate the temporary scratch space pool for helper arguments."""
|
||||
if max_temps == 0:
|
||||
return
|
||||
|
||||
logger.info(f"Allocating temp pool of {max_temps} variables")
|
||||
for i in range(max_temps):
|
||||
temp_name = f"__helper_temp_{i}"
|
||||
temp_var = builder.alloca(ir.IntType(64), name=temp_name)
|
||||
temp_var.align = 8
|
||||
local_sym_tab[temp_name] = LocalSymbol(temp_var, ir.IntType(64))
|
||||
108
pythonbpf/assign_pass.py
Normal file
108
pythonbpf/assign_pass.py
Normal file
@ -0,0 +1,108 @@
|
||||
import ast
|
||||
import logging
|
||||
from llvmlite import ir
|
||||
from pythonbpf.expr import eval_expr
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def handle_struct_field_assignment(
|
||||
func, module, builder, target, rval, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
):
|
||||
"""Handle struct field assignment (obj.field = value)."""
|
||||
|
||||
var_name = target.value.id
|
||||
field_name = target.attr
|
||||
|
||||
if var_name not in local_sym_tab:
|
||||
logger.error(f"Variable '{var_name}' not found in symbol table")
|
||||
return
|
||||
|
||||
struct_type = local_sym_tab[var_name].metadata
|
||||
struct_info = structs_sym_tab[struct_type]
|
||||
|
||||
if field_name not in struct_info.fields:
|
||||
logger.error(f"Field '{field_name}' not found in struct '{struct_type}'")
|
||||
return
|
||||
|
||||
# Get field pointer and evaluate value
|
||||
field_ptr = struct_info.gep(builder, local_sym_tab[var_name].var, field_name)
|
||||
val = eval_expr(
|
||||
func, module, builder, rval, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
)
|
||||
|
||||
if val is None:
|
||||
logger.error(f"Failed to evaluate value for {var_name}.{field_name}")
|
||||
return
|
||||
|
||||
# TODO: Handle string assignment to char array (not a priority)
|
||||
field_type = struct_info.field_type(field_name)
|
||||
if isinstance(field_type, ir.ArrayType) and val[1] == ir.PointerType(ir.IntType(8)):
|
||||
logger.warning(
|
||||
f"String to char array assignment not implemented for {var_name}.{field_name}"
|
||||
)
|
||||
return
|
||||
|
||||
# Store the value
|
||||
builder.store(val[0], field_ptr)
|
||||
logger.info(f"Assigned to struct field {var_name}.{field_name}")
|
||||
|
||||
|
||||
def handle_variable_assignment(
|
||||
func, module, builder, var_name, rval, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
):
|
||||
"""Handle single named variable assignment."""
|
||||
|
||||
if var_name not in local_sym_tab:
|
||||
logger.error(f"Variable {var_name} not declared.")
|
||||
return False
|
||||
|
||||
var_ptr = local_sym_tab[var_name].var
|
||||
var_type = local_sym_tab[var_name].ir_type
|
||||
|
||||
# NOTE: Special case for struct initialization
|
||||
if isinstance(rval, ast.Call) and isinstance(rval.func, ast.Name):
|
||||
struct_name = rval.func.id
|
||||
if struct_name in structs_sym_tab and len(rval.args) == 0:
|
||||
struct_info = structs_sym_tab[struct_name]
|
||||
ir_struct = struct_info.ir_type
|
||||
|
||||
builder.store(ir.Constant(ir_struct, None), var_ptr)
|
||||
logger.info(f"Initialized struct {struct_name} for variable {var_name}")
|
||||
return True
|
||||
|
||||
val_result = eval_expr(
|
||||
func, module, builder, rval, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
)
|
||||
if val_result is None:
|
||||
logger.error(f"Failed to evaluate value for {var_name}")
|
||||
return False
|
||||
|
||||
val, val_type = val_result
|
||||
logger.info(f"Evaluated value for {var_name}: {val} of type {val_type}, {var_type}")
|
||||
if val_type != var_type:
|
||||
if isinstance(val_type, ir.IntType) and isinstance(var_type, ir.IntType):
|
||||
# Allow implicit int widening
|
||||
if val_type.width < var_type.width:
|
||||
val = builder.sext(val, var_type)
|
||||
logger.info(f"Implicitly widened int for variable {var_name}")
|
||||
elif val_type.width > var_type.width:
|
||||
val = builder.trunc(val, var_type)
|
||||
logger.info(f"Implicitly truncated int for variable {var_name}")
|
||||
elif isinstance(val_type, ir.IntType) and isinstance(var_type, ir.PointerType):
|
||||
# NOTE: This is assignment to a PTR_TO_MAP_VALUE_OR_NULL
|
||||
logger.info(
|
||||
f"Creating temporary variable for pointer assignment to {var_name}"
|
||||
)
|
||||
var_ptr_tmp = local_sym_tab[f"{var_name}_tmp"].var
|
||||
builder.store(val, var_ptr_tmp)
|
||||
val = var_ptr_tmp
|
||||
else:
|
||||
logger.error(
|
||||
f"Type mismatch for variable {var_name}: {val_type} vs {var_type}"
|
||||
)
|
||||
return False
|
||||
|
||||
builder.store(val, var_ptr)
|
||||
logger.info(f"Assigned value to variable {var_name}")
|
||||
return True
|
||||
@ -1,82 +0,0 @@
|
||||
import ast
|
||||
from llvmlite import ir
|
||||
|
||||
|
||||
def recursive_dereferencer(var, builder):
|
||||
""" dereference until primitive type comes out"""
|
||||
if var.type == ir.PointerType(ir.PointerType(ir.IntType(64))):
|
||||
a = builder.load(var)
|
||||
return recursive_dereferencer(a, builder)
|
||||
elif var.type == ir.PointerType(ir.IntType(64)):
|
||||
a = builder.load(var)
|
||||
return recursive_dereferencer(a, builder)
|
||||
elif var.type == ir.IntType(64):
|
||||
return var
|
||||
else:
|
||||
raise TypeError(f"Unsupported type for dereferencing: {var.type}")
|
||||
|
||||
|
||||
def handle_binary_op(rval, module, builder, var_name, local_sym_tab, map_sym_tab, func):
|
||||
print(module)
|
||||
left = rval.left
|
||||
right = rval.right
|
||||
op = rval.op
|
||||
|
||||
# Handle left operand
|
||||
if isinstance(left, ast.Name):
|
||||
if left.id in local_sym_tab:
|
||||
left = recursive_dereferencer(local_sym_tab[left.id][0], builder)
|
||||
else:
|
||||
raise SyntaxError(f"Undefined variable: {left.id}")
|
||||
elif isinstance(left, ast.Constant):
|
||||
left = ir.Constant(ir.IntType(64), left.value)
|
||||
else:
|
||||
raise SyntaxError("Unsupported left operand type")
|
||||
|
||||
if isinstance(right, ast.Name):
|
||||
if right.id in local_sym_tab:
|
||||
right = recursive_dereferencer(local_sym_tab[right.id][0], builder)
|
||||
else:
|
||||
raise SyntaxError(f"Undefined variable: {right.id}")
|
||||
elif isinstance(right, ast.Constant):
|
||||
right = ir.Constant(ir.IntType(64), right.value)
|
||||
else:
|
||||
raise SyntaxError("Unsupported right operand type")
|
||||
|
||||
print(f"left is {left}, right is {right}, op is {op}")
|
||||
|
||||
if isinstance(op, ast.Add):
|
||||
builder.store(builder.add(left, right),
|
||||
local_sym_tab[var_name][0])
|
||||
elif isinstance(op, ast.Sub):
|
||||
builder.store(builder.sub(left, right),
|
||||
local_sym_tab[var_name][0])
|
||||
elif isinstance(op, ast.Mult):
|
||||
builder.store(builder.mul(left, right),
|
||||
local_sym_tab[var_name][0])
|
||||
elif isinstance(op, ast.Div):
|
||||
builder.store(builder.sdiv(left, right),
|
||||
local_sym_tab[var_name][0])
|
||||
elif isinstance(op, ast.Mod):
|
||||
builder.store(builder.srem(left, right),
|
||||
local_sym_tab[var_name][0])
|
||||
elif isinstance(op, ast.LShift):
|
||||
builder.store(builder.shl(left, right),
|
||||
local_sym_tab[var_name][0])
|
||||
elif isinstance(op, ast.RShift):
|
||||
builder.store(builder.lshr(left, right),
|
||||
local_sym_tab[var_name][0])
|
||||
elif isinstance(op, ast.BitOr):
|
||||
builder.store(builder.or_(left, right),
|
||||
local_sym_tab[var_name][0])
|
||||
elif isinstance(op, ast.BitXor):
|
||||
builder.store(builder.xor(left, right),
|
||||
local_sym_tab[var_name][0])
|
||||
elif isinstance(op, ast.BitAnd):
|
||||
builder.store(builder.and_(left, right),
|
||||
local_sym_tab[var_name][0])
|
||||
elif isinstance(op, ast.FloorDiv):
|
||||
builder.store(builder.udiv(left, right),
|
||||
local_sym_tab[var_name][0])
|
||||
else:
|
||||
raise SyntaxError("Unsupported binary operation")
|
||||
@ -1,464 +0,0 @@
|
||||
import ast
|
||||
from llvmlite import ir
|
||||
from .expr_pass import eval_expr
|
||||
|
||||
|
||||
def bpf_ktime_get_ns_emitter(call, map_ptr, module, builder, func, local_sym_tab=None, local_var_metadata=None):
|
||||
"""
|
||||
Emit LLVM IR for bpf_ktime_get_ns helper function call.
|
||||
"""
|
||||
# func is an arg to just have a uniform signature with other emitters
|
||||
helper_id = ir.Constant(ir.IntType(64), 5)
|
||||
fn_type = ir.FunctionType(ir.IntType(64), [], var_arg=False)
|
||||
fn_ptr_type = ir.PointerType(fn_type)
|
||||
fn_ptr = builder.inttoptr(helper_id, fn_ptr_type)
|
||||
result = builder.call(fn_ptr, [], tail=False)
|
||||
return result, ir.IntType(64)
|
||||
|
||||
|
||||
def bpf_map_lookup_elem_emitter(call, map_ptr, module, builder, func, local_sym_tab=None, struct_sym_tab=None, local_var_metadata=None):
|
||||
"""
|
||||
Emit LLVM IR for bpf_map_lookup_elem helper function call.
|
||||
"""
|
||||
if call.args and len(call.args) != 1:
|
||||
raise ValueError("Map lookup expects exactly one argument, got "
|
||||
f"{len(call.args)}")
|
||||
key_arg = call.args[0]
|
||||
if isinstance(key_arg, ast.Name):
|
||||
key_name = key_arg.id
|
||||
if local_sym_tab and key_name in local_sym_tab:
|
||||
key_ptr = local_sym_tab[key_name][0]
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Key variable {key_name} not found in local symbol table.")
|
||||
elif isinstance(key_arg, ast.Constant) and isinstance(key_arg.value, int):
|
||||
# handle constant integer keys
|
||||
key_val = key_arg.value
|
||||
key_type = ir.IntType(64)
|
||||
key_ptr = builder.alloca(key_type)
|
||||
key_ptr.align = key_type // 8
|
||||
builder.store(ir.Constant(key_type, key_val), key_ptr)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Only simple variable names are supported as keys in map lookup.")
|
||||
|
||||
if key_ptr is None:
|
||||
raise ValueError("Key pointer is None.")
|
||||
|
||||
map_void_ptr = builder.bitcast(map_ptr, ir.PointerType())
|
||||
|
||||
fn_type = ir.FunctionType(
|
||||
ir.PointerType(), # Return type: void*
|
||||
[ir.PointerType(), ir.PointerType()], # Args: (void*, void*)
|
||||
var_arg=False
|
||||
)
|
||||
fn_ptr_type = ir.PointerType(fn_type)
|
||||
|
||||
# Helper ID 1 is bpf_map_lookup_elem
|
||||
fn_addr = ir.Constant(ir.IntType(64), 1)
|
||||
fn_ptr = builder.inttoptr(fn_addr, fn_ptr_type)
|
||||
|
||||
result = builder.call(fn_ptr, [map_void_ptr, key_ptr], tail=False)
|
||||
|
||||
return result, ir.PointerType()
|
||||
|
||||
|
||||
def bpf_printk_emitter(call, map_ptr, module, builder, func, local_sym_tab=None, local_var_metadata=None):
|
||||
if not hasattr(func, "_fmt_counter"):
|
||||
func._fmt_counter = 0
|
||||
|
||||
if not call.args:
|
||||
raise ValueError("print expects at least one argument")
|
||||
|
||||
if isinstance(call.args[0], ast.JoinedStr):
|
||||
fmt_parts = []
|
||||
exprs = []
|
||||
|
||||
for value in call.args[0].values:
|
||||
print("Value in f-string:", ast.dump(value))
|
||||
if isinstance(value, ast.Constant):
|
||||
if isinstance(value.value, str):
|
||||
fmt_parts.append(value.value)
|
||||
elif isinstance(value.value, int):
|
||||
fmt_parts.append("%lld")
|
||||
exprs.append(ir.Constant(ir.IntType(64), value.value))
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Only string and integer constants are supported in f-string.")
|
||||
elif isinstance(value, ast.FormattedValue):
|
||||
print("Formatted value:", ast.dump(value))
|
||||
# TODO: Dirty handling here, only checks for int or str
|
||||
if isinstance(value.value, ast.Name):
|
||||
if local_sym_tab and value.value.id in local_sym_tab:
|
||||
var_ptr, var_type = local_sym_tab[value.value.id]
|
||||
if isinstance(var_type, ir.IntType):
|
||||
fmt_parts.append("%lld")
|
||||
exprs.append(value.value)
|
||||
elif var_type == ir.PointerType(ir.IntType(8)):
|
||||
# Case with string
|
||||
fmt_parts.append("%s")
|
||||
exprs.append(value.value)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Only integer and pointer types are supported in formatted values.")
|
||||
print("Formatted value variable:", var_ptr, var_type)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Variable {value.value.id} not found in local symbol table.")
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Only simple variable names are supported in formatted values.")
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Unsupported value type in f-string.")
|
||||
|
||||
fmt_str = "".join(fmt_parts) + "\n" + "\0"
|
||||
fmt_name = f"{func.name}____fmt{func._fmt_counter}"
|
||||
func._fmt_counter += 1
|
||||
|
||||
fmt_gvar = ir.GlobalVariable(
|
||||
module, ir.ArrayType(ir.IntType(8), len(fmt_str)), name=fmt_name)
|
||||
fmt_gvar.global_constant = True
|
||||
fmt_gvar.initializer = ir.Constant(
|
||||
ir.ArrayType(ir.IntType(8), len(fmt_str)),
|
||||
bytearray(fmt_str.encode("utf8"))
|
||||
)
|
||||
fmt_gvar.linkage = "internal"
|
||||
fmt_gvar.align = 1
|
||||
|
||||
fmt_ptr = builder.bitcast(fmt_gvar, ir.PointerType())
|
||||
|
||||
args = [fmt_ptr, ir.Constant(ir.IntType(32), len(fmt_str))]
|
||||
|
||||
# Only 3 args supported in bpf_printk
|
||||
if len(exprs) > 3:
|
||||
print(
|
||||
"Warning: bpf_printk supports up to 3 arguments, extra arguments will be ignored.")
|
||||
|
||||
for expr in exprs[:3]:
|
||||
val, _ = eval_expr(func, module, builder,
|
||||
expr, local_sym_tab, None)
|
||||
if val:
|
||||
if isinstance(val.type, ir.PointerType):
|
||||
val = builder.ptrtoint(val, ir.IntType(64))
|
||||
elif isinstance(val.type, ir.IntType):
|
||||
if val.type.width < 64:
|
||||
val = builder.sext(val, ir.IntType(64))
|
||||
else:
|
||||
print(
|
||||
"Warning: Only integer and pointer types are supported in bpf_printk arguments. Others will be converted to 0.")
|
||||
val = ir.Constant(ir.IntType(64), 0)
|
||||
args.append(val)
|
||||
else:
|
||||
print(
|
||||
"Warning: Failed to evaluate expression for bpf_printk argument. It will be converted to 0.")
|
||||
args.append(ir.Constant(ir.IntType(64), 0))
|
||||
fn_type = ir.FunctionType(ir.IntType(
|
||||
64), [ir.PointerType(), ir.IntType(32)], var_arg=True)
|
||||
fn_ptr_type = ir.PointerType(fn_type)
|
||||
fn_addr = ir.Constant(ir.IntType(64), 6)
|
||||
fn_ptr = builder.inttoptr(fn_addr, fn_ptr_type)
|
||||
return builder.call(fn_ptr, args, tail=True)
|
||||
|
||||
for arg in call.args:
|
||||
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
|
||||
fmt_str = arg.value + "\n" + "\0"
|
||||
fmt_name = f"{func.name}____fmt{func._fmt_counter}"
|
||||
func._fmt_counter += 1
|
||||
|
||||
fmt_gvar = ir.GlobalVariable(
|
||||
module, ir.ArrayType(ir.IntType(8), len(fmt_str)), name=fmt_name)
|
||||
fmt_gvar.global_constant = True
|
||||
fmt_gvar.initializer = ir.Constant( # type: ignore
|
||||
ir.ArrayType(ir.IntType(8), len(fmt_str)),
|
||||
bytearray(fmt_str.encode("utf8"))
|
||||
)
|
||||
fmt_gvar.linkage = "internal"
|
||||
fmt_gvar.align = 1 # type: ignore
|
||||
|
||||
fmt_ptr = builder.bitcast(fmt_gvar, ir.PointerType())
|
||||
|
||||
fn_type = ir.FunctionType(ir.IntType(
|
||||
64), [ir.PointerType(), ir.IntType(32)], var_arg=True)
|
||||
fn_ptr_type = ir.PointerType(fn_type)
|
||||
fn_addr = ir.Constant(ir.IntType(64), 6)
|
||||
fn_ptr = builder.inttoptr(fn_addr, fn_ptr_type)
|
||||
|
||||
builder.call(fn_ptr, [fmt_ptr, ir.Constant(
|
||||
ir.IntType(32), len(fmt_str))], tail=True)
|
||||
|
||||
|
||||
def bpf_map_update_elem_emitter(call, map_ptr, module, builder, func, local_sym_tab=None, struct_sym_tab=None, local_var_metadata=None):
|
||||
"""
|
||||
Emit LLVM IR for bpf_map_update_elem helper function call.
|
||||
Expected call signature: map.update(key, value, flags=0)
|
||||
"""
|
||||
if not call.args or len(call.args) < 2 or len(call.args) > 3:
|
||||
raise ValueError("Map update expects 2 or 3 arguments (key, value, flags), got "
|
||||
f"{len(call.args)}")
|
||||
|
||||
key_arg = call.args[0]
|
||||
value_arg = call.args[1]
|
||||
flags_arg = call.args[2] if len(call.args) > 2 else None
|
||||
|
||||
# Handle key
|
||||
if isinstance(key_arg, ast.Name):
|
||||
key_name = key_arg.id
|
||||
if local_sym_tab and key_name in local_sym_tab:
|
||||
key_ptr = local_sym_tab[key_name][0]
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Key variable {key_name} not found in local symbol table.")
|
||||
elif isinstance(key_arg, ast.Constant) and isinstance(key_arg.value, int):
|
||||
# Handle constant integer keys
|
||||
key_val = key_arg.value
|
||||
key_type = ir.IntType(64)
|
||||
key_ptr = builder.alloca(key_type)
|
||||
key_ptr.align = key_type.width // 8
|
||||
builder.store(ir.Constant(key_type, key_val), key_ptr)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Only simple variable names and integer constants are supported as keys in map update.")
|
||||
|
||||
# Handle value
|
||||
if isinstance(value_arg, ast.Name):
|
||||
value_name = value_arg.id
|
||||
if local_sym_tab and value_name in local_sym_tab:
|
||||
value_ptr = local_sym_tab[value_name][0]
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Value variable {value_name} not found in local symbol table.")
|
||||
elif isinstance(value_arg, ast.Constant) and isinstance(value_arg.value, int):
|
||||
# Handle constant integers
|
||||
value_val = value_arg.value
|
||||
value_type = ir.IntType(64)
|
||||
value_ptr = builder.alloca(value_type)
|
||||
value_ptr.align = value_type.width // 8
|
||||
builder.store(ir.Constant(value_type, value_val), value_ptr)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Only simple variable names and integer constants are supported as values in map update.")
|
||||
|
||||
# Handle flags argument (defaults to 0)
|
||||
if flags_arg is not None:
|
||||
if isinstance(flags_arg, ast.Constant) and isinstance(flags_arg.value, int):
|
||||
flags_val = flags_arg.value
|
||||
elif isinstance(flags_arg, ast.Name):
|
||||
flags_name = flags_arg.id
|
||||
if local_sym_tab and flags_name in local_sym_tab:
|
||||
# Assume it's a stored integer value, load it
|
||||
flags_ptr = local_sym_tab[flags_name][0]
|
||||
flags_val = builder.load(flags_ptr)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Flags variable {flags_name} not found in local symbol table.")
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Only integer constants and simple variable names are supported as flags in map update.")
|
||||
else:
|
||||
flags_val = 0
|
||||
|
||||
if key_ptr is None or value_ptr is None:
|
||||
raise ValueError("Key pointer or value pointer is None.")
|
||||
|
||||
map_void_ptr = builder.bitcast(map_ptr, ir.PointerType())
|
||||
fn_type = ir.FunctionType(
|
||||
ir.IntType(64),
|
||||
[ir.PointerType(), ir.PointerType(), ir.PointerType(), ir.IntType(64)],
|
||||
var_arg=False
|
||||
)
|
||||
fn_ptr_type = ir.PointerType(fn_type)
|
||||
|
||||
# helper id
|
||||
fn_addr = ir.Constant(ir.IntType(64), 2)
|
||||
fn_ptr = builder.inttoptr(fn_addr, fn_ptr_type)
|
||||
|
||||
if isinstance(flags_val, int):
|
||||
flags_const = ir.Constant(ir.IntType(64), flags_val)
|
||||
else:
|
||||
flags_const = flags_val
|
||||
|
||||
result = builder.call(
|
||||
fn_ptr, [map_void_ptr, key_ptr, value_ptr, flags_const], tail=False)
|
||||
|
||||
return result, None
|
||||
|
||||
|
||||
def bpf_map_delete_elem_emitter(call, map_ptr, module, builder, func, local_sym_tab=None, struct_sym_tab=None, local_var_metadata=None):
|
||||
"""
|
||||
Emit LLVM IR for bpf_map_delete_elem helper function call.
|
||||
Expected call signature: map.delete(key)
|
||||
"""
|
||||
# Check for correct number of arguments
|
||||
if not call.args or len(call.args) != 1:
|
||||
raise ValueError("Map delete expects exactly 1 argument (key), got "
|
||||
f"{len(call.args)}")
|
||||
|
||||
key_arg = call.args[0]
|
||||
|
||||
# Handle key argument
|
||||
if isinstance(key_arg, ast.Name):
|
||||
key_name = key_arg.id
|
||||
if local_sym_tab and key_name in local_sym_tab:
|
||||
key_ptr = local_sym_tab[key_name][0]
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Key variable {key_name} not found in local symbol table.")
|
||||
elif isinstance(key_arg, ast.Constant) and isinstance(key_arg.value, int):
|
||||
# Handle constant integer keys
|
||||
key_val = key_arg.value
|
||||
key_type = ir.IntType(64)
|
||||
key_ptr = builder.alloca(key_type)
|
||||
key_ptr.align = key_type.width // 8
|
||||
builder.store(ir.Constant(key_type, key_val), key_ptr)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Only simple variable names and integer constants are supported as keys in map delete.")
|
||||
|
||||
if key_ptr is None:
|
||||
raise ValueError("Key pointer is None.")
|
||||
|
||||
# Cast map pointer to void*
|
||||
map_void_ptr = builder.bitcast(map_ptr, ir.PointerType())
|
||||
|
||||
# Define function type for bpf_map_delete_elem
|
||||
fn_type = ir.FunctionType(
|
||||
ir.IntType(64), # Return type: int64 (status code)
|
||||
[ir.PointerType(), ir.PointerType()], # Args: (void*, void*)
|
||||
var_arg=False
|
||||
)
|
||||
fn_ptr_type = ir.PointerType(fn_type)
|
||||
|
||||
# Helper ID 3 is bpf_map_delete_elem
|
||||
fn_addr = ir.Constant(ir.IntType(64), 3)
|
||||
fn_ptr = builder.inttoptr(fn_addr, fn_ptr_type)
|
||||
|
||||
# Call the helper function
|
||||
result = builder.call(fn_ptr, [map_void_ptr, key_ptr], tail=False)
|
||||
|
||||
return result, None
|
||||
|
||||
|
||||
def bpf_get_current_pid_tgid_emitter(call, map_ptr, module, builder, func, local_sym_tab=None, local_var_metadata=None):
|
||||
"""
|
||||
Emit LLVM IR for bpf_get_current_pid_tgid helper function call.
|
||||
"""
|
||||
# func is an arg to just have a uniform signature with other emitters
|
||||
helper_id = ir.Constant(ir.IntType(64), 14)
|
||||
fn_type = ir.FunctionType(ir.IntType(64), [], var_arg=False)
|
||||
fn_ptr_type = ir.PointerType(fn_type)
|
||||
fn_ptr = builder.inttoptr(helper_id, fn_ptr_type)
|
||||
result = builder.call(fn_ptr, [], tail=False)
|
||||
|
||||
# Extract the lower 32 bits (PID) using bitwise AND with 0xFFFFFFFF
|
||||
mask = ir.Constant(ir.IntType(64), 0xFFFFFFFF)
|
||||
pid = builder.and_(result, mask)
|
||||
return pid, ir.IntType(64)
|
||||
|
||||
|
||||
def bpf_perf_event_output_handler(call, map_ptr, module, builder, func, local_sym_tab=None, struct_sym_tab=None, local_var_metadata=None):
|
||||
if len(call.args) != 1:
|
||||
raise ValueError("Perf event output expects exactly one argument (data), got "
|
||||
f"{len(call.args)}")
|
||||
data_arg = call.args[0]
|
||||
ctx_ptr = func.args[0] # First argument to the function is ctx
|
||||
|
||||
if isinstance(data_arg, ast.Name):
|
||||
data_name = data_arg.id
|
||||
if local_sym_tab and data_name in local_sym_tab:
|
||||
data_ptr = local_sym_tab[data_name][0]
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Data variable {data_name} not found in local symbol table.")
|
||||
# Check is data_name is a struct
|
||||
if local_var_metadata and data_name in local_var_metadata:
|
||||
data_type = local_var_metadata[data_name]
|
||||
if data_type in struct_sym_tab:
|
||||
struct_info = struct_sym_tab[data_type]
|
||||
size_val = ir.Constant(ir.IntType(64), struct_info["size"])
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Struct type {data_type} for variable {data_name} not found in struct symbol table.")
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Metadata for variable {data_name} not found in local variable metadata.")
|
||||
|
||||
# BPF_F_CURRENT_CPU is -1 in 32 bit
|
||||
flags_val = ir.Constant(ir.IntType(64), 0xFFFFFFFF)
|
||||
|
||||
map_void_ptr = builder.bitcast(map_ptr, ir.PointerType())
|
||||
data_void_ptr = builder.bitcast(data_ptr, ir.PointerType())
|
||||
fn_type = ir.FunctionType(
|
||||
ir.IntType(64),
|
||||
[ir.PointerType(ir.IntType(8)), ir.PointerType(), ir.IntType(64),
|
||||
ir.PointerType(), ir.IntType(64)],
|
||||
var_arg=False
|
||||
)
|
||||
fn_ptr_type = ir.PointerType(fn_type)
|
||||
|
||||
# helper id
|
||||
fn_addr = ir.Constant(ir.IntType(64), 25)
|
||||
fn_ptr = builder.inttoptr(fn_addr, fn_ptr_type)
|
||||
|
||||
result = builder.call(
|
||||
fn_ptr, [ctx_ptr, map_void_ptr, flags_val, data_void_ptr, size_val], tail=False)
|
||||
return result, None
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Only simple object names are supported as data in perf event output.")
|
||||
|
||||
|
||||
helper_func_list = {
|
||||
"lookup": bpf_map_lookup_elem_emitter,
|
||||
"print": bpf_printk_emitter,
|
||||
"ktime": bpf_ktime_get_ns_emitter,
|
||||
"update": bpf_map_update_elem_emitter,
|
||||
"delete": bpf_map_delete_elem_emitter,
|
||||
"pid": bpf_get_current_pid_tgid_emitter,
|
||||
"output": bpf_perf_event_output_handler,
|
||||
}
|
||||
|
||||
|
||||
def handle_helper_call(call, module, builder, func, local_sym_tab=None, map_sym_tab=None, struct_sym_tab=None, local_var_metadata=None):
|
||||
if isinstance(call.func, ast.Name):
|
||||
func_name = call.func.id
|
||||
if func_name in helper_func_list:
|
||||
# it is not a map method call
|
||||
return helper_func_list[func_name](call, None, module, builder, func, local_sym_tab)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Function {func_name} is not implemented as a helper function.")
|
||||
elif isinstance(call.func, ast.Attribute):
|
||||
# likely a map method call
|
||||
if isinstance(call.func.value, ast.Call) and isinstance(call.func.value.func, ast.Name):
|
||||
map_name = call.func.value.func.id
|
||||
method_name = call.func.attr
|
||||
if map_sym_tab and map_name in map_sym_tab:
|
||||
map_ptr = map_sym_tab[map_name]
|
||||
if method_name in helper_func_list:
|
||||
print(local_var_metadata)
|
||||
return helper_func_list[method_name](
|
||||
call, map_ptr, module, builder, func, local_sym_tab, struct_sym_tab, local_var_metadata)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Map method {method_name} is not implemented as a helper function.")
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Map variable {map_name} not found in symbol tables.")
|
||||
elif isinstance(call.func.value, ast.Name):
|
||||
obj_name = call.func.value.id
|
||||
method_name = call.func.attr
|
||||
if map_sym_tab and obj_name in map_sym_tab:
|
||||
map_ptr = map_sym_tab[obj_name]
|
||||
if method_name in helper_func_list:
|
||||
return helper_func_list[method_name](
|
||||
call, map_ptr, module, builder, func, local_sym_tab, struct_sym_tab, local_var_metadata)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Map method {method_name} is not implemented as a helper function.")
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Map variable {obj_name} not found in symbol tables.")
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Attribute not supported for map method calls.")
|
||||
@ -1,15 +1,38 @@
|
||||
import ast
|
||||
from llvmlite import ir
|
||||
from .license_pass import license_processing
|
||||
from .functions_pass import func_proc
|
||||
from .maps_pass import maps_proc
|
||||
from .structs_pass import structs_proc
|
||||
from .globals_pass import globals_processing
|
||||
from .functions import func_proc
|
||||
from .maps import maps_proc
|
||||
from .structs import structs_proc
|
||||
from .vmlinux_parser import vmlinux_proc
|
||||
from .globals_pass import (
|
||||
globals_list_creation,
|
||||
globals_processing,
|
||||
populate_global_symbol_table,
|
||||
)
|
||||
from .debuginfo import DW_LANG_C11, DwarfBehaviorEnum, DebugInfoGenerator
|
||||
import os
|
||||
import subprocess
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
from pylibbpf import BpfProgram
|
||||
import tempfile
|
||||
from logging import Logger
|
||||
import logging
|
||||
import re
|
||||
|
||||
logger: Logger = logging.getLogger(__name__)
|
||||
|
||||
VERSION = "v0.1.4"
|
||||
|
||||
|
||||
def finalize_module(original_str):
|
||||
"""After all IR generation is complete, we monkey patch btf_ama attribute"""
|
||||
|
||||
# Create a string with applied transformation of btf_ama attribute addition to BTF struct field accesses.
|
||||
pattern = r'(@"llvm\.[^"]+:[^"]*" = external global i64, !llvm\.preserve\.access\.index ![0-9]+)'
|
||||
replacement = r'\1 "btf_ama"'
|
||||
return re.sub(pattern, replacement, original_str)
|
||||
|
||||
|
||||
def find_bpf_chunks(tree):
|
||||
@ -26,21 +49,28 @@ def find_bpf_chunks(tree):
|
||||
|
||||
def processor(source_code, filename, module):
|
||||
tree = ast.parse(source_code, filename)
|
||||
print(ast.dump(tree, indent=4))
|
||||
logger.debug(ast.dump(tree, indent=4))
|
||||
|
||||
bpf_chunks = find_bpf_chunks(tree)
|
||||
for func_node in bpf_chunks:
|
||||
print(f"Found BPF function/struct: {func_node.name}")
|
||||
logger.info(f"Found BPF function/struct: {func_node.name}")
|
||||
|
||||
vmlinux_proc(tree, module)
|
||||
populate_global_symbol_table(tree, module)
|
||||
license_processing(tree, module)
|
||||
globals_processing(tree, module)
|
||||
|
||||
structs_sym_tab = structs_proc(tree, module, bpf_chunks)
|
||||
map_sym_tab = maps_proc(tree, module, bpf_chunks)
|
||||
func_proc(tree, module, bpf_chunks, map_sym_tab, structs_sym_tab)
|
||||
|
||||
license_processing(tree, module)
|
||||
globals_processing(tree, module)
|
||||
globals_list_creation(tree, module)
|
||||
|
||||
|
||||
def compile_to_ir(filename: str, output: str):
|
||||
def compile_to_ir(filename: str, output: str, loglevel=logging.INFO):
|
||||
logging.basicConfig(
|
||||
level=loglevel, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
||||
)
|
||||
with open(filename) as f:
|
||||
source = f.read()
|
||||
|
||||
@ -48,61 +78,98 @@ def compile_to_ir(filename: str, output: str):
|
||||
module.data_layout = "e-m:e-p:64:64-i64:64-i128:128-n32:64-S128"
|
||||
module.triple = "bpf"
|
||||
|
||||
if not hasattr(module, '_debug_compile_unit'):
|
||||
module._file_metadata = module.add_debug_info("DIFile", { # type: ignore
|
||||
"filename": filename,
|
||||
"directory": os.path.dirname(filename)
|
||||
})
|
||||
|
||||
module._debug_compile_unit = module.add_debug_info("DICompileUnit", { # type: ignore
|
||||
"language": 29, # DW_LANG_C11
|
||||
"file": module._file_metadata, # type: ignore
|
||||
"producer": "PythonBPF DSL Compiler",
|
||||
"isOptimized": True,
|
||||
"runtimeVersion": 0,
|
||||
"emissionKind": 1,
|
||||
"splitDebugInlining": False,
|
||||
"nameTableKind": 0
|
||||
}, is_distinct=True)
|
||||
|
||||
module.add_named_metadata(
|
||||
"llvm.dbg.cu", module._debug_compile_unit) # type: ignore
|
||||
if not hasattr(module, "_debug_compile_unit"):
|
||||
debug_generator = DebugInfoGenerator(module)
|
||||
debug_generator.generate_file_metadata(filename, os.path.dirname(filename))
|
||||
debug_generator.generate_debug_cu(
|
||||
DW_LANG_C11,
|
||||
f"PythonBPF {VERSION}",
|
||||
True, # TODO: This is probably not true
|
||||
# TODO: add a global field here that keeps track of all the globals. Works without it, but I think it might
|
||||
# be required for kprobes.
|
||||
True,
|
||||
)
|
||||
|
||||
processor(source, filename, module)
|
||||
|
||||
wchar_size = module.add_metadata([ir.Constant(ir.IntType(32), 1),
|
||||
"wchar_size",
|
||||
ir.Constant(ir.IntType(32), 4)])
|
||||
frame_pointer = module.add_metadata([ir.Constant(ir.IntType(32), 7),
|
||||
"frame-pointer",
|
||||
ir.Constant(ir.IntType(32), 2)])
|
||||
wchar_size = module.add_metadata(
|
||||
[
|
||||
DwarfBehaviorEnum.ERROR_IF_MISMATCH,
|
||||
"wchar_size",
|
||||
ir.Constant(ir.IntType(32), 4),
|
||||
]
|
||||
)
|
||||
frame_pointer = module.add_metadata(
|
||||
[
|
||||
DwarfBehaviorEnum.OVERRIDE_USE_LARGEST,
|
||||
"frame-pointer",
|
||||
ir.Constant(ir.IntType(32), 2),
|
||||
]
|
||||
)
|
||||
# Add Debug Info Version (3 = DWARF v3, which LLVM expects)
|
||||
debug_info_version = module.add_metadata([ir.Constant(ir.IntType(32), 2),
|
||||
"Debug Info Version",
|
||||
ir.Constant(ir.IntType(32), 3)])
|
||||
debug_info_version = module.add_metadata(
|
||||
[
|
||||
DwarfBehaviorEnum.WARNING_IF_MISMATCH,
|
||||
"Debug Info Version",
|
||||
ir.Constant(ir.IntType(32), 3),
|
||||
]
|
||||
)
|
||||
|
||||
# Add explicit DWARF version (4 is common, works with LLVM BPF backend)
|
||||
dwarf_version = module.add_metadata([ir.Constant(ir.IntType(32), 2),
|
||||
"Dwarf Version",
|
||||
ir.Constant(ir.IntType(32), 4)])
|
||||
# Add explicit DWARF version 5
|
||||
dwarf_version = module.add_metadata(
|
||||
[
|
||||
DwarfBehaviorEnum.OVERRIDE_USE_LARGEST,
|
||||
"Dwarf Version",
|
||||
ir.Constant(ir.IntType(32), 5),
|
||||
]
|
||||
)
|
||||
|
||||
module.add_named_metadata("llvm.module.flags", wchar_size)
|
||||
module.add_named_metadata("llvm.module.flags", frame_pointer)
|
||||
module.add_named_metadata("llvm.module.flags", debug_info_version)
|
||||
module.add_named_metadata("llvm.module.flags", dwarf_version)
|
||||
|
||||
module.add_named_metadata("llvm.ident", ["llvmlite PythonBPF v0.0.1"])
|
||||
module.add_named_metadata("llvm.ident", [f"PythonBPF {VERSION}"])
|
||||
|
||||
print(f"IR written to {output}")
|
||||
module_string = finalize_module(str(module))
|
||||
|
||||
logger.info(f"IR written to {output}")
|
||||
with open(output, "w") as f:
|
||||
f.write(f"source_filename = \"{filename}\"\n")
|
||||
f.write(str(module))
|
||||
f.write(f'source_filename = "{filename}"\n')
|
||||
f.write(module_string)
|
||||
f.write("\n")
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def compile():
|
||||
def _run_llc(ll_file, obj_file):
|
||||
"""Compile LLVM IR to BPF object file using llc."""
|
||||
|
||||
logger.info(f"Compiling IR to object: {ll_file} -> {obj_file}")
|
||||
result = subprocess.run(
|
||||
[
|
||||
"llc",
|
||||
"-march=bpf",
|
||||
"-filetype=obj",
|
||||
"-O2",
|
||||
str(ll_file),
|
||||
"-o",
|
||||
str(obj_file),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
logger.info(f"Object file written to {obj_file}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"llc compilation failed: {result.stderr}")
|
||||
return False
|
||||
|
||||
|
||||
def compile(loglevel=logging.INFO) -> bool:
|
||||
# Look one level up the stack to the caller of this function
|
||||
caller_frame = inspect.stack()[1]
|
||||
caller_file = Path(caller_frame.filename).resolve()
|
||||
@ -110,26 +177,31 @@ def compile():
|
||||
ll_file = Path("/tmp") / caller_file.with_suffix(".ll").name
|
||||
o_file = caller_file.with_suffix(".o")
|
||||
|
||||
compile_to_ir(str(caller_file), str(ll_file))
|
||||
success = True
|
||||
success = (
|
||||
compile_to_ir(str(caller_file), str(ll_file), loglevel=loglevel) and success
|
||||
)
|
||||
|
||||
subprocess.run([
|
||||
"llc", "-march=bpf", "-filetype=obj", "-O2",
|
||||
str(ll_file), "-o", str(o_file)
|
||||
], check=True)
|
||||
success = _run_llc(ll_file, o_file) and success
|
||||
|
||||
print(f"Object written to {o_file}, {ll_file} can be removed")
|
||||
logger.info(f"Object written to {o_file}")
|
||||
return success
|
||||
|
||||
|
||||
def BPF() -> BpfProgram:
|
||||
def BPF(loglevel=logging.INFO) -> BpfProgram:
|
||||
caller_frame = inspect.stack()[1]
|
||||
caller_file = Path(caller_frame.filename).resolve()
|
||||
ll_file = Path("/tmp") / caller_file.with_suffix(".ll").name
|
||||
o_file = Path("/tmp") / caller_file.with_suffix(".o").name
|
||||
compile_to_ir(str(caller_file), str(ll_file))
|
||||
src = inspect.getsource(caller_frame.frame)
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w+", delete=True, suffix=".py"
|
||||
) as f, tempfile.NamedTemporaryFile(
|
||||
mode="w+", delete=True, suffix=".ll"
|
||||
) as inter, tempfile.NamedTemporaryFile(
|
||||
mode="w+", delete=False, suffix=".o"
|
||||
) as obj_file:
|
||||
f.write(src)
|
||||
f.flush()
|
||||
source = f.name
|
||||
compile_to_ir(source, str(inter.name), loglevel=loglevel)
|
||||
_run_llc(str(inter.name), str(obj_file.name))
|
||||
|
||||
subprocess.run([
|
||||
"llc", "-march=bpf", "-filetype=obj", "-O2",
|
||||
str(ll_file), "-o", str(o_file)
|
||||
], check=True)
|
||||
|
||||
return BpfProgram(str(o_file))
|
||||
return BpfProgram(str(obj_file.name))
|
||||
|
||||
5
pythonbpf/debuginfo/__init__.py
Normal file
5
pythonbpf/debuginfo/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from .dwarf_constants import * # noqa: F403
|
||||
from .dtypes import * # noqa: F403
|
||||
from .debug_info_generator import DebugInfoGenerator
|
||||
|
||||
__all__ = ["DebugInfoGenerator"]
|
||||
139
pythonbpf/debuginfo/debug_info_generator.py
Normal file
139
pythonbpf/debuginfo/debug_info_generator.py
Normal file
@ -0,0 +1,139 @@
|
||||
"""
|
||||
Debug information generation module for Python-BPF
|
||||
Provides utilities for generating DWARF/BTF debug information
|
||||
"""
|
||||
|
||||
from . import dwarf_constants as dc
|
||||
from typing import Any, List
|
||||
|
||||
|
||||
class DebugInfoGenerator:
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
self._type_cache = {} # Cache for common debug types
|
||||
|
||||
def generate_file_metadata(self, filename, dirname):
|
||||
self.module._file_metadata = self.module.add_debug_info(
|
||||
"DIFile",
|
||||
{ # type: ignore
|
||||
"filename": filename,
|
||||
"directory": dirname,
|
||||
},
|
||||
)
|
||||
|
||||
def generate_debug_cu(
|
||||
self, language, producer: str, is_optimized: bool, is_distinct: bool
|
||||
):
|
||||
self.module._debug_compile_unit = self.module.add_debug_info(
|
||||
"DICompileUnit",
|
||||
{ # type: ignore
|
||||
"language": language,
|
||||
"file": self.module._file_metadata, # type: ignore
|
||||
"producer": producer,
|
||||
"isOptimized": is_optimized,
|
||||
"runtimeVersion": 0,
|
||||
"emissionKind": 1,
|
||||
"splitDebugInlining": False,
|
||||
"nameTableKind": 0,
|
||||
},
|
||||
is_distinct=is_distinct,
|
||||
)
|
||||
self.module.add_named_metadata("llvm.dbg.cu", self.module._debug_compile_unit) # type: ignore
|
||||
|
||||
def get_basic_type(self, name: str, size: int, encoding: int) -> Any:
|
||||
"""Get or create a basic type with caching"""
|
||||
key = (name, size, encoding)
|
||||
if key not in self._type_cache:
|
||||
self._type_cache[key] = self.module.add_debug_info(
|
||||
"DIBasicType", {"name": name, "size": size, "encoding": encoding}
|
||||
)
|
||||
return self._type_cache[key]
|
||||
|
||||
def get_int32_type(self) -> Any:
|
||||
"""Get debug info for signed 32-bit integer"""
|
||||
return self.get_basic_type("int", 32, dc.DW_ATE_signed)
|
||||
|
||||
def get_uint32_type(self) -> Any:
|
||||
"""Get debug info for unsigned 32-bit integer"""
|
||||
return self.get_basic_type("unsigned int", 32, dc.DW_ATE_unsigned)
|
||||
|
||||
def get_uint64_type(self) -> Any:
|
||||
"""Get debug info for unsigned 64-bit integer"""
|
||||
return self.get_basic_type("unsigned long long", 64, dc.DW_ATE_unsigned)
|
||||
|
||||
def create_pointer_type(self, base_type: Any, size: int = 64) -> Any:
|
||||
"""Create a pointer type to the given base type"""
|
||||
return self.module.add_debug_info(
|
||||
"DIDerivedType",
|
||||
{"tag": dc.DW_TAG_pointer_type, "baseType": base_type, "size": size},
|
||||
)
|
||||
|
||||
def create_array_type(self, base_type: Any, count: int) -> Any:
|
||||
"""Create an array type of the given base type with specified count"""
|
||||
subrange = self.module.add_debug_info("DISubrange", {"count": count})
|
||||
return self.module.add_debug_info(
|
||||
"DICompositeType",
|
||||
{
|
||||
"tag": dc.DW_TAG_array_type,
|
||||
"baseType": base_type,
|
||||
"size": self._compute_array_size(base_type, count),
|
||||
"elements": [subrange],
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _compute_array_size(base_type: Any, count: int) -> int:
|
||||
# Extract size from base_type if possible
|
||||
# For simplicity, assuming base_type has a size attribute
|
||||
return getattr(base_type, "size", 32) * count
|
||||
|
||||
def create_struct_member(self, name: str, base_type: Any, offset: int) -> Any:
|
||||
"""Create a struct member with the given name, type, and offset"""
|
||||
return self.module.add_debug_info(
|
||||
"DIDerivedType",
|
||||
{
|
||||
"tag": dc.DW_TAG_member,
|
||||
"name": name,
|
||||
"file": self.module._file_metadata,
|
||||
"baseType": base_type,
|
||||
"size": getattr(base_type, "size", 64),
|
||||
"offset": offset,
|
||||
},
|
||||
)
|
||||
|
||||
def create_struct_type(
|
||||
self, members: List[Any], size: int, is_distinct: bool
|
||||
) -> Any:
|
||||
"""Create a struct type with the given members and size"""
|
||||
return self.module.add_debug_info(
|
||||
"DICompositeType",
|
||||
{
|
||||
"tag": dc.DW_TAG_structure_type,
|
||||
"file": self.module._file_metadata,
|
||||
"size": size,
|
||||
"elements": members,
|
||||
},
|
||||
is_distinct=is_distinct,
|
||||
)
|
||||
|
||||
def create_global_var_debug_info(
|
||||
self, name: str, var_type: Any, is_local: bool = False
|
||||
) -> Any:
|
||||
"""Create debug info for a global variable"""
|
||||
global_var = self.module.add_debug_info(
|
||||
"DIGlobalVariable",
|
||||
{
|
||||
"name": name,
|
||||
"scope": self.module._debug_compile_unit,
|
||||
"file": self.module._file_metadata,
|
||||
"type": var_type,
|
||||
"isLocal": is_local,
|
||||
"isDefinition": True,
|
||||
},
|
||||
is_distinct=True,
|
||||
)
|
||||
|
||||
return self.module.add_debug_info(
|
||||
"DIGlobalVariableExpression",
|
||||
{"var": global_var, "expr": self.module.add_debug_info("DIExpression", {})},
|
||||
)
|
||||
7
pythonbpf/debuginfo/dtypes.py
Normal file
7
pythonbpf/debuginfo/dtypes.py
Normal file
@ -0,0 +1,7 @@
|
||||
import llvmlite.ir as ir
|
||||
|
||||
|
||||
class DwarfBehaviorEnum:
|
||||
ERROR_IF_MISMATCH = ir.Constant(ir.IntType(32), 1)
|
||||
WARNING_IF_MISMATCH = ir.Constant(ir.IntType(32), 2)
|
||||
OVERRIDE_USE_LARGEST = ir.Constant(ir.IntType(32), 7)
|
||||
@ -7,7 +7,7 @@ DW_UT_skeleton = 0x04
|
||||
DW_UT_split_compile = 0x05
|
||||
DW_UT_split_type = 0x06
|
||||
DW_UT_lo_user = 0x80
|
||||
DW_UT_hi_user = 0xff
|
||||
DW_UT_hi_user = 0xFF
|
||||
|
||||
DW_TAG_array_type = 0x01
|
||||
DW_TAG_class_type = 0x02
|
||||
@ -15,10 +15,10 @@ DW_TAG_entry_point = 0x03
|
||||
DW_TAG_enumeration_type = 0x04
|
||||
DW_TAG_formal_parameter = 0x05
|
||||
DW_TAG_imported_declaration = 0x08
|
||||
DW_TAG_label = 0x0a
|
||||
DW_TAG_lexical_block = 0x0b
|
||||
DW_TAG_member = 0x0d
|
||||
DW_TAG_pointer_type = 0x0f
|
||||
DW_TAG_label = 0x0A
|
||||
DW_TAG_lexical_block = 0x0B
|
||||
DW_TAG_member = 0x0D
|
||||
DW_TAG_pointer_type = 0x0F
|
||||
DW_TAG_reference_type = 0x10
|
||||
DW_TAG_compile_unit = 0x11
|
||||
DW_TAG_string_type = 0x12
|
||||
@ -28,12 +28,12 @@ DW_TAG_typedef = 0x16
|
||||
DW_TAG_union_type = 0x17
|
||||
DW_TAG_unspecified_parameters = 0x18
|
||||
DW_TAG_variant = 0x19
|
||||
DW_TAG_common_block = 0x1a
|
||||
DW_TAG_common_inclusion = 0x1b
|
||||
DW_TAG_inheritance = 0x1c
|
||||
DW_TAG_inlined_subroutine = 0x1d
|
||||
DW_TAG_module = 0x1e
|
||||
DW_TAG_ptr_to_member_type = 0x1f
|
||||
DW_TAG_common_block = 0x1A
|
||||
DW_TAG_common_inclusion = 0x1B
|
||||
DW_TAG_inheritance = 0x1C
|
||||
DW_TAG_inlined_subroutine = 0x1D
|
||||
DW_TAG_module = 0x1E
|
||||
DW_TAG_ptr_to_member_type = 0x1F
|
||||
DW_TAG_set_type = 0x20
|
||||
DW_TAG_subrange_type = 0x21
|
||||
DW_TAG_with_stmt = 0x22
|
||||
@ -44,12 +44,12 @@ DW_TAG_const_type = 0x26
|
||||
DW_TAG_constant = 0x27
|
||||
DW_TAG_enumerator = 0x28
|
||||
DW_TAG_file_type = 0x29
|
||||
DW_TAG_friend = 0x2a
|
||||
DW_TAG_namelist = 0x2b
|
||||
DW_TAG_namelist_item = 0x2c
|
||||
DW_TAG_packed_type = 0x2d
|
||||
DW_TAG_subprogram = 0x2e
|
||||
DW_TAG_template_type_parameter = 0x2f
|
||||
DW_TAG_friend = 0x2A
|
||||
DW_TAG_namelist = 0x2B
|
||||
DW_TAG_namelist_item = 0x2C
|
||||
DW_TAG_packed_type = 0x2D
|
||||
DW_TAG_subprogram = 0x2E
|
||||
DW_TAG_template_type_parameter = 0x2F
|
||||
DW_TAG_template_value_parameter = 0x30
|
||||
DW_TAG_thrown_type = 0x31
|
||||
DW_TAG_try_block = 0x32
|
||||
@ -60,11 +60,11 @@ DW_TAG_dwarf_procedure = 0x36
|
||||
DW_TAG_restrict_type = 0x37
|
||||
DW_TAG_interface_type = 0x38
|
||||
DW_TAG_namespace = 0x39
|
||||
DW_TAG_imported_module = 0x3a
|
||||
DW_TAG_unspecified_type = 0x3b
|
||||
DW_TAG_partial_unit = 0x3c
|
||||
DW_TAG_imported_unit = 0x3d
|
||||
DW_TAG_condition = 0x3f
|
||||
DW_TAG_imported_module = 0x3A
|
||||
DW_TAG_unspecified_type = 0x3B
|
||||
DW_TAG_partial_unit = 0x3C
|
||||
DW_TAG_imported_unit = 0x3D
|
||||
DW_TAG_condition = 0x3F
|
||||
DW_TAG_shared_type = 0x40
|
||||
DW_TAG_type_unit = 0x41
|
||||
DW_TAG_rvalue_reference_type = 0x42
|
||||
@ -75,8 +75,8 @@ DW_TAG_dynamic_type = 0x46
|
||||
DW_TAG_atomic_type = 0x47
|
||||
DW_TAG_call_site = 0x48
|
||||
DW_TAG_call_site_parameter = 0x49
|
||||
DW_TAG_skeleton_unit = 0x4a
|
||||
DW_TAG_immutable_type = 0x4b
|
||||
DW_TAG_skeleton_unit = 0x4A
|
||||
DW_TAG_immutable_type = 0x4B
|
||||
DW_TAG_lo_user = 0x4080
|
||||
DW_TAG_MIPS_loop = 0x4081
|
||||
DW_TAG_format_label = 0x4101
|
||||
@ -88,8 +88,8 @@ DW_TAG_GNU_template_template_param = 0x4106
|
||||
DW_TAG_GNU_template_parameter_pack = 0x4107
|
||||
DW_TAG_GNU_formal_parameter_pack = 0x4108
|
||||
DW_TAG_GNU_call_site = 0x4109
|
||||
DW_TAG_GNU_call_site_parameter = 0x410a
|
||||
DW_TAG_hi_user = 0xffff
|
||||
DW_TAG_GNU_call_site_parameter = 0x410A
|
||||
DW_TAG_hi_user = 0xFFFF
|
||||
|
||||
DW_CHILDREN_no = 0
|
||||
DW_CHILDREN_yes = 1
|
||||
@ -98,9 +98,9 @@ DW_AT_sibling = 0x01
|
||||
DW_AT_location = 0x02
|
||||
DW_AT_name = 0x03
|
||||
DW_AT_ordering = 0x09
|
||||
DW_AT_byte_size = 0x0b
|
||||
DW_AT_bit_offset = 0x0c
|
||||
DW_AT_bit_size = 0x0d
|
||||
DW_AT_byte_size = 0x0B
|
||||
DW_AT_bit_offset = 0x0C
|
||||
DW_AT_bit_size = 0x0D
|
||||
DW_AT_stmt_list = 0x10
|
||||
DW_AT_low_pc = 0x11
|
||||
DW_AT_high_pc = 0x12
|
||||
@ -110,20 +110,20 @@ DW_AT_discr_value = 0x16
|
||||
DW_AT_visibility = 0x17
|
||||
DW_AT_import = 0x18
|
||||
DW_AT_string_length = 0x19
|
||||
DW_AT_common_reference = 0x1a
|
||||
DW_AT_comp_dir = 0x1b
|
||||
DW_AT_const_value = 0x1c
|
||||
DW_AT_containing_type = 0x1d
|
||||
DW_AT_default_value = 0x1e
|
||||
DW_AT_common_reference = 0x1A
|
||||
DW_AT_comp_dir = 0x1B
|
||||
DW_AT_const_value = 0x1C
|
||||
DW_AT_containing_type = 0x1D
|
||||
DW_AT_default_value = 0x1E
|
||||
DW_AT_inline = 0x20
|
||||
DW_AT_is_optional = 0x21
|
||||
DW_AT_lower_bound = 0x22
|
||||
DW_AT_producer = 0x25
|
||||
DW_AT_prototyped = 0x27
|
||||
DW_AT_return_addr = 0x2a
|
||||
DW_AT_start_scope = 0x2c
|
||||
DW_AT_bit_stride = 0x2e
|
||||
DW_AT_upper_bound = 0x2f
|
||||
DW_AT_return_addr = 0x2A
|
||||
DW_AT_start_scope = 0x2C
|
||||
DW_AT_bit_stride = 0x2E
|
||||
DW_AT_upper_bound = 0x2F
|
||||
DW_AT_abstract_origin = 0x31
|
||||
DW_AT_accessibility = 0x32
|
||||
DW_AT_address_class = 0x33
|
||||
@ -133,12 +133,12 @@ DW_AT_calling_convention = 0x36
|
||||
DW_AT_count = 0x37
|
||||
DW_AT_data_member_location = 0x38
|
||||
DW_AT_decl_column = 0x39
|
||||
DW_AT_decl_file = 0x3a
|
||||
DW_AT_decl_line = 0x3b
|
||||
DW_AT_declaration = 0x3c
|
||||
DW_AT_discr_list = 0x3d
|
||||
DW_AT_encoding = 0x3e
|
||||
DW_AT_external = 0x3f
|
||||
DW_AT_decl_file = 0x3A
|
||||
DW_AT_decl_line = 0x3B
|
||||
DW_AT_declaration = 0x3C
|
||||
DW_AT_discr_list = 0x3D
|
||||
DW_AT_encoding = 0x3E
|
||||
DW_AT_external = 0x3F
|
||||
DW_AT_frame_base = 0x40
|
||||
DW_AT_friend = 0x41
|
||||
DW_AT_identifier_case = 0x42
|
||||
@ -149,12 +149,12 @@ DW_AT_segment = 0x46
|
||||
DW_AT_specification = 0x47
|
||||
DW_AT_static_link = 0x48
|
||||
DW_AT_type = 0x49
|
||||
DW_AT_use_location = 0x4a
|
||||
DW_AT_variable_parameter = 0x4b
|
||||
DW_AT_virtuality = 0x4c
|
||||
DW_AT_vtable_elem_location = 0x4d
|
||||
DW_AT_allocated = 0x4e
|
||||
DW_AT_associated = 0x4f
|
||||
DW_AT_use_location = 0x4A
|
||||
DW_AT_variable_parameter = 0x4B
|
||||
DW_AT_virtuality = 0x4C
|
||||
DW_AT_vtable_elem_location = 0x4D
|
||||
DW_AT_allocated = 0x4E
|
||||
DW_AT_associated = 0x4F
|
||||
DW_AT_data_location = 0x50
|
||||
DW_AT_byte_stride = 0x51
|
||||
DW_AT_entry_pc = 0x52
|
||||
@ -165,12 +165,12 @@ DW_AT_trampoline = 0x56
|
||||
DW_AT_call_column = 0x57
|
||||
DW_AT_call_file = 0x58
|
||||
DW_AT_call_line = 0x59
|
||||
DW_AT_description = 0x5a
|
||||
DW_AT_binary_scale = 0x5b
|
||||
DW_AT_decimal_scale = 0x5c
|
||||
DW_AT_small = 0x5d
|
||||
DW_AT_decimal_sign = 0x5e
|
||||
DW_AT_digit_count = 0x5f
|
||||
DW_AT_description = 0x5A
|
||||
DW_AT_binary_scale = 0x5B
|
||||
DW_AT_decimal_scale = 0x5C
|
||||
DW_AT_small = 0x5D
|
||||
DW_AT_decimal_sign = 0x5E
|
||||
DW_AT_digit_count = 0x5F
|
||||
DW_AT_picture_string = 0x60
|
||||
DW_AT_mutable = 0x61
|
||||
DW_AT_threads_scaled = 0x62
|
||||
@ -181,12 +181,12 @@ DW_AT_elemental = 0x66
|
||||
DW_AT_pure = 0x67
|
||||
DW_AT_recursive = 0x68
|
||||
DW_AT_signature = 0x69
|
||||
DW_AT_main_subprogram = 0x6a
|
||||
DW_AT_data_bit_offset = 0x6b
|
||||
DW_AT_const_expr = 0x6c
|
||||
DW_AT_enum_class = 0x6d
|
||||
DW_AT_linkage_name = 0x6e
|
||||
DW_AT_string_length_bit_size = 0x6f
|
||||
DW_AT_main_subprogram = 0x6A
|
||||
DW_AT_data_bit_offset = 0x6B
|
||||
DW_AT_const_expr = 0x6C
|
||||
DW_AT_enum_class = 0x6D
|
||||
DW_AT_linkage_name = 0x6E
|
||||
DW_AT_string_length_bit_size = 0x6F
|
||||
DW_AT_string_length_byte_size = 0x70
|
||||
DW_AT_rank = 0x71
|
||||
DW_AT_str_offsets_base = 0x72
|
||||
@ -196,12 +196,12 @@ DW_AT_dwo_name = 0x76
|
||||
DW_AT_reference = 0x77
|
||||
DW_AT_rvalue_reference = 0x78
|
||||
DW_AT_macros = 0x79
|
||||
DW_AT_call_all_calls = 0x7a
|
||||
DW_AT_call_all_source_calls = 0x7b
|
||||
DW_AT_call_all_tail_calls = 0x7c
|
||||
DW_AT_call_return_pc = 0x7d
|
||||
DW_AT_call_value = 0x7e
|
||||
DW_AT_call_origin = 0x7f
|
||||
DW_AT_call_all_calls = 0x7A
|
||||
DW_AT_call_all_source_calls = 0x7B
|
||||
DW_AT_call_all_tail_calls = 0x7C
|
||||
DW_AT_call_return_pc = 0x7D
|
||||
DW_AT_call_value = 0x7E
|
||||
DW_AT_call_origin = 0x7F
|
||||
DW_AT_call_parameter = 0x80
|
||||
DW_AT_call_pc = 0x81
|
||||
DW_AT_call_tail_call = 0x82
|
||||
@ -212,9 +212,9 @@ DW_AT_call_data_value = 0x86
|
||||
DW_AT_noreturn = 0x87
|
||||
DW_AT_alignment = 0x88
|
||||
DW_AT_export_symbols = 0x89
|
||||
DW_AT_deleted = 0x8a
|
||||
DW_AT_defaulted = 0x8b
|
||||
DW_AT_loclists_base = 0x8c
|
||||
DW_AT_deleted = 0x8A
|
||||
DW_AT_defaulted = 0x8B
|
||||
DW_AT_loclists_base = 0x8C
|
||||
DW_AT_lo_user = 0x2000
|
||||
DW_AT_MIPS_fde = 0x2001
|
||||
DW_AT_MIPS_loop_begin = 0x2002
|
||||
@ -225,12 +225,12 @@ DW_AT_MIPS_software_pipeline_depth = 0x2006
|
||||
DW_AT_MIPS_linkage_name = 0x2007
|
||||
DW_AT_MIPS_stride = 0x2008
|
||||
DW_AT_MIPS_abstract_name = 0x2009
|
||||
DW_AT_MIPS_clone_origin = 0x200a
|
||||
DW_AT_MIPS_has_inlines = 0x200b
|
||||
DW_AT_MIPS_stride_byte = 0x200c
|
||||
DW_AT_MIPS_stride_elem = 0x200d
|
||||
DW_AT_MIPS_ptr_dopetype = 0x200e
|
||||
DW_AT_MIPS_allocatable_dopetype = 0x200f
|
||||
DW_AT_MIPS_clone_origin = 0x200A
|
||||
DW_AT_MIPS_has_inlines = 0x200B
|
||||
DW_AT_MIPS_stride_byte = 0x200C
|
||||
DW_AT_MIPS_stride_elem = 0x200D
|
||||
DW_AT_MIPS_ptr_dopetype = 0x200E
|
||||
DW_AT_MIPS_allocatable_dopetype = 0x200F
|
||||
DW_AT_MIPS_assumed_shape_dopetype = 0x2010
|
||||
DW_AT_MIPS_assumed_size = 0x2011
|
||||
DW_AT_sf_names = 0x2101
|
||||
@ -242,12 +242,12 @@ DW_AT_body_end = 0x2106
|
||||
DW_AT_GNU_vector = 0x2107
|
||||
DW_AT_GNU_guarded_by = 0x2108
|
||||
DW_AT_GNU_pt_guarded_by = 0x2109
|
||||
DW_AT_GNU_guarded = 0x210a
|
||||
DW_AT_GNU_pt_guarded = 0x210b
|
||||
DW_AT_GNU_locks_excluded = 0x210c
|
||||
DW_AT_GNU_exclusive_locks_required = 0x210d
|
||||
DW_AT_GNU_shared_locks_required = 0x210e
|
||||
DW_AT_GNU_odr_signature = 0x210f
|
||||
DW_AT_GNU_guarded = 0x210A
|
||||
DW_AT_GNU_pt_guarded = 0x210B
|
||||
DW_AT_GNU_locks_excluded = 0x210C
|
||||
DW_AT_GNU_exclusive_locks_required = 0x210D
|
||||
DW_AT_GNU_shared_locks_required = 0x210E
|
||||
DW_AT_GNU_odr_signature = 0x210F
|
||||
DW_AT_GNU_template_name = 0x2110
|
||||
DW_AT_GNU_call_site_value = 0x2111
|
||||
DW_AT_GNU_call_site_data_value = 0x2112
|
||||
@ -260,7 +260,7 @@ DW_AT_GNU_all_source_call_sites = 0x2118
|
||||
DW_AT_GNU_locviews = 0x2137
|
||||
DW_AT_GNU_entry_view = 0x2138
|
||||
DW_AT_GNU_macros = 0x2119
|
||||
DW_AT_GNU_deleted = 0x211a
|
||||
DW_AT_GNU_deleted = 0x211A
|
||||
DW_AT_GNU_dwo_name = 0x2130
|
||||
DW_AT_GNU_dwo_id = 0x2131
|
||||
DW_AT_GNU_ranges_base = 0x2132
|
||||
@ -270,7 +270,7 @@ DW_AT_GNU_pubtypes = 0x2135
|
||||
DW_AT_GNU_numerator = 0x2303
|
||||
DW_AT_GNU_denominator = 0x2304
|
||||
DW_AT_GNU_bias = 0x2305
|
||||
DW_AT_hi_user = 0x3fff
|
||||
DW_AT_hi_user = 0x3FFF
|
||||
|
||||
DW_FORM_addr = 0x01
|
||||
DW_FORM_block2 = 0x03
|
||||
@ -280,12 +280,12 @@ DW_FORM_data4 = 0x06
|
||||
DW_FORM_data8 = 0x07
|
||||
DW_FORM_string = 0x08
|
||||
DW_FORM_block = 0x09
|
||||
DW_FORM_block1 = 0x0a
|
||||
DW_FORM_data1 = 0x0b
|
||||
DW_FORM_flag = 0x0c
|
||||
DW_FORM_sdata = 0x0d
|
||||
DW_FORM_strp = 0x0e
|
||||
DW_FORM_udata = 0x0f
|
||||
DW_FORM_block1 = 0x0A
|
||||
DW_FORM_data1 = 0x0B
|
||||
DW_FORM_flag = 0x0C
|
||||
DW_FORM_sdata = 0x0D
|
||||
DW_FORM_strp = 0x0E
|
||||
DW_FORM_udata = 0x0F
|
||||
DW_FORM_ref_addr = 0x10
|
||||
DW_FORM_ref1 = 0x11
|
||||
DW_FORM_ref2 = 0x12
|
||||
@ -296,12 +296,12 @@ DW_FORM_indirect = 0x16
|
||||
DW_FORM_sec_offset = 0x17
|
||||
DW_FORM_exprloc = 0x18
|
||||
DW_FORM_flag_present = 0x19
|
||||
DW_FORM_strx = 0x1a
|
||||
DW_FORM_addrx = 0x1b
|
||||
DW_FORM_ref_sup4 = 0x1c
|
||||
DW_FORM_strp_sup = 0x1d
|
||||
DW_FORM_data16 = 0x1e
|
||||
DW_FORM_line_strp = 0x1f
|
||||
DW_FORM_strx = 0x1A
|
||||
DW_FORM_addrx = 0x1B
|
||||
DW_FORM_ref_sup4 = 0x1C
|
||||
DW_FORM_strp_sup = 0x1D
|
||||
DW_FORM_data16 = 0x1E
|
||||
DW_FORM_line_strp = 0x1F
|
||||
DW_FORM_ref_sig8 = 0x20
|
||||
DW_FORM_implicit_const = 0x21
|
||||
DW_FORM_loclistx = 0x22
|
||||
@ -312,24 +312,24 @@ DW_FORM_strx2 = 0x26
|
||||
DW_FORM_strx3 = 0x27
|
||||
DW_FORM_strx4 = 0x28
|
||||
DW_FORM_addrx1 = 0x29
|
||||
DW_FORM_addrx2 = 0x2a
|
||||
DW_FORM_addrx3 = 0x2b
|
||||
DW_FORM_addrx4 = 0x2c
|
||||
DW_FORM_GNU_addr_index = 0x1f01
|
||||
DW_FORM_GNU_str_index = 0x1f02
|
||||
DW_FORM_GNU_ref_alt = 0x1f20
|
||||
DW_FORM_GNU_strp_alt = 0x1f21
|
||||
DW_FORM_addrx2 = 0x2A
|
||||
DW_FORM_addrx3 = 0x2B
|
||||
DW_FORM_addrx4 = 0x2C
|
||||
DW_FORM_GNU_addr_index = 0x1F01
|
||||
DW_FORM_GNU_str_index = 0x1F02
|
||||
DW_FORM_GNU_ref_alt = 0x1F20
|
||||
DW_FORM_GNU_strp_alt = 0x1F21
|
||||
|
||||
DW_OP_addr = 0x03
|
||||
DW_OP_deref = 0x06
|
||||
DW_OP_const1u = 0x08
|
||||
DW_OP_const1s = 0x09
|
||||
DW_OP_const2u = 0x0a
|
||||
DW_OP_const2s = 0x0b
|
||||
DW_OP_const4u = 0x0c
|
||||
DW_OP_const4s = 0x0d
|
||||
DW_OP_const8u = 0x0e
|
||||
DW_OP_const8s = 0x0f
|
||||
DW_OP_const2u = 0x0A
|
||||
DW_OP_const2s = 0x0B
|
||||
DW_OP_const4u = 0x0C
|
||||
DW_OP_const4s = 0x0D
|
||||
DW_OP_const8u = 0x0E
|
||||
DW_OP_const8s = 0x0F
|
||||
DW_OP_constu = 0x10
|
||||
DW_OP_consts = 0x11
|
||||
DW_OP_dup = 0x12
|
||||
@ -340,12 +340,12 @@ DW_OP_swap = 0x16
|
||||
DW_OP_rot = 0x17
|
||||
DW_OP_xderef = 0x18
|
||||
DW_OP_abs = 0x19
|
||||
DW_OP_and = 0x1a
|
||||
DW_OP_div = 0x1b
|
||||
DW_OP_minus = 0x1c
|
||||
DW_OP_mod = 0x1d
|
||||
DW_OP_mul = 0x1e
|
||||
DW_OP_neg = 0x1f
|
||||
DW_OP_and = 0x1A
|
||||
DW_OP_div = 0x1B
|
||||
DW_OP_minus = 0x1C
|
||||
DW_OP_mod = 0x1D
|
||||
DW_OP_mul = 0x1E
|
||||
DW_OP_neg = 0x1F
|
||||
DW_OP_not = 0x20
|
||||
DW_OP_or = 0x21
|
||||
DW_OP_plus = 0x22
|
||||
@ -356,12 +356,12 @@ DW_OP_shra = 0x26
|
||||
DW_OP_xor = 0x27
|
||||
DW_OP_bra = 0x28
|
||||
DW_OP_eq = 0x29
|
||||
DW_OP_ge = 0x2a
|
||||
DW_OP_gt = 0x2b
|
||||
DW_OP_le = 0x2c
|
||||
DW_OP_lt = 0x2d
|
||||
DW_OP_ne = 0x2e
|
||||
DW_OP_skip = 0x2f
|
||||
DW_OP_ge = 0x2A
|
||||
DW_OP_gt = 0x2B
|
||||
DW_OP_le = 0x2C
|
||||
DW_OP_lt = 0x2D
|
||||
DW_OP_ne = 0x2E
|
||||
DW_OP_skip = 0x2F
|
||||
DW_OP_lit0 = 0x30
|
||||
DW_OP_lit1 = 0x31
|
||||
DW_OP_lit2 = 0x32
|
||||
@ -372,12 +372,12 @@ DW_OP_lit6 = 0x36
|
||||
DW_OP_lit7 = 0x37
|
||||
DW_OP_lit8 = 0x38
|
||||
DW_OP_lit9 = 0x39
|
||||
DW_OP_lit10 = 0x3a
|
||||
DW_OP_lit11 = 0x3b
|
||||
DW_OP_lit12 = 0x3c
|
||||
DW_OP_lit13 = 0x3d
|
||||
DW_OP_lit14 = 0x3e
|
||||
DW_OP_lit15 = 0x3f
|
||||
DW_OP_lit10 = 0x3A
|
||||
DW_OP_lit11 = 0x3B
|
||||
DW_OP_lit12 = 0x3C
|
||||
DW_OP_lit13 = 0x3D
|
||||
DW_OP_lit14 = 0x3E
|
||||
DW_OP_lit15 = 0x3F
|
||||
DW_OP_lit16 = 0x40
|
||||
DW_OP_lit17 = 0x41
|
||||
DW_OP_lit18 = 0x42
|
||||
@ -388,12 +388,12 @@ DW_OP_lit22 = 0x46
|
||||
DW_OP_lit23 = 0x47
|
||||
DW_OP_lit24 = 0x48
|
||||
DW_OP_lit25 = 0x49
|
||||
DW_OP_lit26 = 0x4a
|
||||
DW_OP_lit27 = 0x4b
|
||||
DW_OP_lit28 = 0x4c
|
||||
DW_OP_lit29 = 0x4d
|
||||
DW_OP_lit30 = 0x4e
|
||||
DW_OP_lit31 = 0x4f
|
||||
DW_OP_lit26 = 0x4A
|
||||
DW_OP_lit27 = 0x4B
|
||||
DW_OP_lit28 = 0x4C
|
||||
DW_OP_lit29 = 0x4D
|
||||
DW_OP_lit30 = 0x4E
|
||||
DW_OP_lit31 = 0x4F
|
||||
DW_OP_reg0 = 0x50
|
||||
DW_OP_reg1 = 0x51
|
||||
DW_OP_reg2 = 0x52
|
||||
@ -404,12 +404,12 @@ DW_OP_reg6 = 0x56
|
||||
DW_OP_reg7 = 0x57
|
||||
DW_OP_reg8 = 0x58
|
||||
DW_OP_reg9 = 0x59
|
||||
DW_OP_reg10 = 0x5a
|
||||
DW_OP_reg11 = 0x5b
|
||||
DW_OP_reg12 = 0x5c
|
||||
DW_OP_reg13 = 0x5d
|
||||
DW_OP_reg14 = 0x5e
|
||||
DW_OP_reg15 = 0x5f
|
||||
DW_OP_reg10 = 0x5A
|
||||
DW_OP_reg11 = 0x5B
|
||||
DW_OP_reg12 = 0x5C
|
||||
DW_OP_reg13 = 0x5D
|
||||
DW_OP_reg14 = 0x5E
|
||||
DW_OP_reg15 = 0x5F
|
||||
DW_OP_reg16 = 0x60
|
||||
DW_OP_reg17 = 0x61
|
||||
DW_OP_reg18 = 0x62
|
||||
@ -420,12 +420,12 @@ DW_OP_reg22 = 0x66
|
||||
DW_OP_reg23 = 0x67
|
||||
DW_OP_reg24 = 0x68
|
||||
DW_OP_reg25 = 0x69
|
||||
DW_OP_reg26 = 0x6a
|
||||
DW_OP_reg27 = 0x6b
|
||||
DW_OP_reg28 = 0x6c
|
||||
DW_OP_reg29 = 0x6d
|
||||
DW_OP_reg30 = 0x6e
|
||||
DW_OP_reg31 = 0x6f
|
||||
DW_OP_reg26 = 0x6A
|
||||
DW_OP_reg27 = 0x6B
|
||||
DW_OP_reg28 = 0x6C
|
||||
DW_OP_reg29 = 0x6D
|
||||
DW_OP_reg30 = 0x6E
|
||||
DW_OP_reg31 = 0x6F
|
||||
DW_OP_breg0 = 0x70
|
||||
DW_OP_breg1 = 0x71
|
||||
DW_OP_breg2 = 0x72
|
||||
@ -436,12 +436,12 @@ DW_OP_breg6 = 0x76
|
||||
DW_OP_breg7 = 0x77
|
||||
DW_OP_breg8 = 0x78
|
||||
DW_OP_breg9 = 0x79
|
||||
DW_OP_breg10 = 0x7a
|
||||
DW_OP_breg11 = 0x7b
|
||||
DW_OP_breg12 = 0x7c
|
||||
DW_OP_breg13 = 0x7d
|
||||
DW_OP_breg14 = 0x7e
|
||||
DW_OP_breg15 = 0x7f
|
||||
DW_OP_breg10 = 0x7A
|
||||
DW_OP_breg11 = 0x7B
|
||||
DW_OP_breg12 = 0x7C
|
||||
DW_OP_breg13 = 0x7D
|
||||
DW_OP_breg14 = 0x7E
|
||||
DW_OP_breg15 = 0x7F
|
||||
DW_OP_breg16 = 0x80
|
||||
DW_OP_breg17 = 0x81
|
||||
DW_OP_breg18 = 0x82
|
||||
@ -452,12 +452,12 @@ DW_OP_breg22 = 0x86
|
||||
DW_OP_breg23 = 0x87
|
||||
DW_OP_breg24 = 0x88
|
||||
DW_OP_breg25 = 0x89
|
||||
DW_OP_breg26 = 0x8a
|
||||
DW_OP_breg27 = 0x8b
|
||||
DW_OP_breg28 = 0x8c
|
||||
DW_OP_breg29 = 0x8d
|
||||
DW_OP_breg30 = 0x8e
|
||||
DW_OP_breg31 = 0x8f
|
||||
DW_OP_breg26 = 0x8A
|
||||
DW_OP_breg27 = 0x8B
|
||||
DW_OP_breg28 = 0x8C
|
||||
DW_OP_breg29 = 0x8D
|
||||
DW_OP_breg30 = 0x8E
|
||||
DW_OP_breg31 = 0x8F
|
||||
DW_OP_regx = 0x90
|
||||
DW_OP_fbreg = 0x91
|
||||
DW_OP_bregx = 0x92
|
||||
@ -468,38 +468,38 @@ DW_OP_nop = 0x96
|
||||
DW_OP_push_object_address = 0x97
|
||||
DW_OP_call2 = 0x98
|
||||
DW_OP_call4 = 0x99
|
||||
DW_OP_call_ref = 0x9a
|
||||
DW_OP_form_tls_address = 0x9b
|
||||
DW_OP_call_frame_cfa = 0x9c
|
||||
DW_OP_bit_piece = 0x9d
|
||||
DW_OP_implicit_value = 0x9e
|
||||
DW_OP_stack_value = 0x9f
|
||||
DW_OP_implicit_pointer = 0xa0
|
||||
DW_OP_addrx = 0xa1
|
||||
DW_OP_constx = 0xa2
|
||||
DW_OP_entry_value = 0xa3
|
||||
DW_OP_const_type = 0xa4
|
||||
DW_OP_regval_type = 0xa5
|
||||
DW_OP_deref_type = 0xa6
|
||||
DW_OP_xderef_type = 0xa7
|
||||
DW_OP_convert = 0xa8
|
||||
DW_OP_reinterpret = 0xa9
|
||||
DW_OP_GNU_push_tls_address = 0xe0
|
||||
DW_OP_GNU_uninit = 0xf0
|
||||
DW_OP_GNU_encoded_addr = 0xf1
|
||||
DW_OP_GNU_implicit_pointer = 0xf2
|
||||
DW_OP_GNU_entry_value = 0xf3
|
||||
DW_OP_GNU_const_type = 0xf4
|
||||
DW_OP_GNU_regval_type = 0xf5
|
||||
DW_OP_GNU_deref_type = 0xf6
|
||||
DW_OP_GNU_convert = 0xf7
|
||||
DW_OP_GNU_reinterpret = 0xf9
|
||||
DW_OP_GNU_parameter_ref = 0xfa
|
||||
DW_OP_GNU_addr_index = 0xfb
|
||||
DW_OP_GNU_const_index = 0xfc
|
||||
DW_OP_GNU_variable_value = 0xfd
|
||||
DW_OP_lo_user = 0xe0
|
||||
DW_OP_hi_user = 0xff
|
||||
DW_OP_call_ref = 0x9A
|
||||
DW_OP_form_tls_address = 0x9B
|
||||
DW_OP_call_frame_cfa = 0x9C
|
||||
DW_OP_bit_piece = 0x9D
|
||||
DW_OP_implicit_value = 0x9E
|
||||
DW_OP_stack_value = 0x9F
|
||||
DW_OP_implicit_pointer = 0xA0
|
||||
DW_OP_addrx = 0xA1
|
||||
DW_OP_constx = 0xA2
|
||||
DW_OP_entry_value = 0xA3
|
||||
DW_OP_const_type = 0xA4
|
||||
DW_OP_regval_type = 0xA5
|
||||
DW_OP_deref_type = 0xA6
|
||||
DW_OP_xderef_type = 0xA7
|
||||
DW_OP_convert = 0xA8
|
||||
DW_OP_reinterpret = 0xA9
|
||||
DW_OP_GNU_push_tls_address = 0xE0
|
||||
DW_OP_GNU_uninit = 0xF0
|
||||
DW_OP_GNU_encoded_addr = 0xF1
|
||||
DW_OP_GNU_implicit_pointer = 0xF2
|
||||
DW_OP_GNU_entry_value = 0xF3
|
||||
DW_OP_GNU_const_type = 0xF4
|
||||
DW_OP_GNU_regval_type = 0xF5
|
||||
DW_OP_GNU_deref_type = 0xF6
|
||||
DW_OP_GNU_convert = 0xF7
|
||||
DW_OP_GNU_reinterpret = 0xF9
|
||||
DW_OP_GNU_parameter_ref = 0xFA
|
||||
DW_OP_GNU_addr_index = 0xFB
|
||||
DW_OP_GNU_const_index = 0xFC
|
||||
DW_OP_GNU_variable_value = 0xFD
|
||||
DW_OP_lo_user = 0xE0
|
||||
DW_OP_hi_user = 0xFF
|
||||
|
||||
DW_ATE_void = 0x0
|
||||
DW_ATE_address = 0x1
|
||||
@ -511,17 +511,17 @@ DW_ATE_signed_char = 0x6
|
||||
DW_ATE_unsigned = 0x7
|
||||
DW_ATE_unsigned_char = 0x8
|
||||
DW_ATE_imaginary_float = 0x9
|
||||
DW_ATE_packed_decimal = 0xa
|
||||
DW_ATE_numeric_string = 0xb
|
||||
DW_ATE_edited = 0xc
|
||||
DW_ATE_signed_fixed = 0xd
|
||||
DW_ATE_unsigned_fixed = 0xe
|
||||
DW_ATE_decimal_float = 0xf
|
||||
DW_ATE_packed_decimal = 0xA
|
||||
DW_ATE_numeric_string = 0xB
|
||||
DW_ATE_edited = 0xC
|
||||
DW_ATE_signed_fixed = 0xD
|
||||
DW_ATE_unsigned_fixed = 0xE
|
||||
DW_ATE_decimal_float = 0xF
|
||||
DW_ATE_UTF = 0x10
|
||||
DW_ATE_UCS = 0x11
|
||||
DW_ATE_ASCII = 0x12
|
||||
DW_ATE_lo_user = 0x80
|
||||
DW_ATE_hi_user = 0xff
|
||||
DW_ATE_hi_user = 0xFF
|
||||
|
||||
DW_DS_unsigned = 1
|
||||
DW_DS_leading_overpunch = 2
|
||||
@ -533,7 +533,7 @@ DW_END_default = 0
|
||||
DW_END_big = 1
|
||||
DW_END_little = 2
|
||||
DW_END_lo_user = 0x40
|
||||
DW_END_hi_user = 0xff
|
||||
DW_END_hi_user = 0xFF
|
||||
|
||||
DW_ACCESS_public = 1
|
||||
DW_ACCESS_protected = 2
|
||||
@ -556,12 +556,12 @@ DW_LANG_Cobol85 = 0x0006
|
||||
DW_LANG_Fortran77 = 0x0007
|
||||
DW_LANG_Fortran90 = 0x0008
|
||||
DW_LANG_Pascal83 = 0x0009
|
||||
DW_LANG_Modula2 = 0x000a
|
||||
DW_LANG_Java = 0x000b
|
||||
DW_LANG_C99 = 0x000c
|
||||
DW_LANG_Ada95 = 0x000d
|
||||
DW_LANG_Fortran95 = 0x000e
|
||||
DW_LANG_PLI = 0x000f
|
||||
DW_LANG_Modula2 = 0x000A
|
||||
DW_LANG_Java = 0x000B
|
||||
DW_LANG_C99 = 0x000C
|
||||
DW_LANG_Ada95 = 0x000D
|
||||
DW_LANG_Fortran95 = 0x000E
|
||||
DW_LANG_PLI = 0x000F
|
||||
DW_LANG_ObjC = 0x0010
|
||||
DW_LANG_ObjC_plus_plus = 0x0011
|
||||
DW_LANG_UPC = 0x0012
|
||||
@ -572,12 +572,12 @@ DW_LANG_Go = 0x0016
|
||||
DW_LANG_Modula3 = 0x0017
|
||||
DW_LANG_Haskell = 0x0018
|
||||
DW_LANG_C_plus_plus_03 = 0x0019
|
||||
DW_LANG_C_plus_plus_11 = 0x001a
|
||||
DW_LANG_OCaml = 0x001b
|
||||
DW_LANG_Rust = 0x001c
|
||||
DW_LANG_C11 = 0x001d
|
||||
DW_LANG_Swift = 0x001e
|
||||
DW_LANG_Julia = 0x001f
|
||||
DW_LANG_C_plus_plus_11 = 0x001A
|
||||
DW_LANG_OCaml = 0x001B
|
||||
DW_LANG_Rust = 0x001C
|
||||
DW_LANG_C11 = 0x001D
|
||||
DW_LANG_Swift = 0x001E
|
||||
DW_LANG_Julia = 0x001F
|
||||
DW_LANG_Dylan = 0x0020
|
||||
DW_LANG_C_plus_plus_14 = 0x0021
|
||||
DW_LANG_Fortran03 = 0x0022
|
||||
@ -586,7 +586,7 @@ DW_LANG_RenderScript = 0x0024
|
||||
DW_LANG_BLISS = 0x0025
|
||||
DW_LANG_lo_user = 0x8000
|
||||
DW_LANG_Mips_Assembler = 0x8001
|
||||
DW_LANG_hi_user = 0xffff
|
||||
DW_LANG_hi_user = 0xFFFF
|
||||
|
||||
DW_ID_case_sensitive = 0
|
||||
DW_ID_up_case = 1
|
||||
@ -599,7 +599,7 @@ DW_CC_nocall = 0x3
|
||||
DW_CC_pass_by_reference = 0x4
|
||||
DW_CC_pass_by_value = 0x5
|
||||
DW_CC_lo_user = 0x40
|
||||
DW_CC_hi_user = 0xff
|
||||
DW_CC_hi_user = 0xFF
|
||||
|
||||
DW_INL_not_inlined = 0
|
||||
DW_INL_inlined = 1
|
||||
@ -622,7 +622,7 @@ DW_LNCT_timestamp = 0x3
|
||||
DW_LNCT_size = 0x4
|
||||
DW_LNCT_MD5 = 0x5
|
||||
DW_LNCT_lo_user = 0x2000
|
||||
DW_LNCT_hi_user = 0x3fff
|
||||
DW_LNCT_hi_user = 0x3FFF
|
||||
|
||||
DW_LNS_copy = 1
|
||||
DW_LNS_advance_pc = 2
|
||||
@ -659,11 +659,11 @@ DW_MACRO_undef_strp = 0x06
|
||||
DW_MACRO_import = 0x07
|
||||
DW_MACRO_define_sup = 0x08
|
||||
DW_MACRO_undef_sup = 0x09
|
||||
DW_MACRO_import_sup = 0x0a
|
||||
DW_MACRO_define_strx = 0x0b
|
||||
DW_MACRO_undef_strx = 0x0c
|
||||
DW_MACRO_lo_user = 0xe0
|
||||
DW_MACRO_hi_user = 0xff
|
||||
DW_MACRO_import_sup = 0x0A
|
||||
DW_MACRO_define_strx = 0x0B
|
||||
DW_MACRO_undef_strx = 0x0C
|
||||
DW_MACRO_lo_user = 0xE0
|
||||
DW_MACRO_hi_user = 0xFF
|
||||
|
||||
DW_RLE_end_of_list = 0x0
|
||||
DW_RLE_base_addressx = 0x1
|
||||
@ -691,7 +691,7 @@ DW_LLE_GNU_start_length_entry = 0x3
|
||||
|
||||
DW_CFA_advance_loc = 0x40
|
||||
DW_CFA_offset = 0x80
|
||||
DW_CFA_restore = 0xc0
|
||||
DW_CFA_restore = 0xC0
|
||||
DW_CFA_extended = 0
|
||||
DW_CFA_nop = 0x00
|
||||
DW_CFA_set_loc = 0x01
|
||||
@ -703,12 +703,12 @@ DW_CFA_restore_extended = 0x06
|
||||
DW_CFA_undefined = 0x07
|
||||
DW_CFA_same_value = 0x08
|
||||
DW_CFA_register = 0x09
|
||||
DW_CFA_remember_state = 0x0a
|
||||
DW_CFA_restore_state = 0x0b
|
||||
DW_CFA_def_cfa = 0x0c
|
||||
DW_CFA_def_cfa_register = 0x0d
|
||||
DW_CFA_def_cfa_offset = 0x0e
|
||||
DW_CFA_def_cfa_expression = 0x0f
|
||||
DW_CFA_remember_state = 0x0A
|
||||
DW_CFA_restore_state = 0x0B
|
||||
DW_CFA_def_cfa = 0x0C
|
||||
DW_CFA_def_cfa_register = 0x0D
|
||||
DW_CFA_def_cfa_offset = 0x0E
|
||||
DW_CFA_def_cfa_expression = 0x0F
|
||||
DW_CFA_expression = 0x10
|
||||
DW_CFA_offset_extended_sf = 0x11
|
||||
DW_CFA_def_cfa_sf = 0x12
|
||||
@ -716,26 +716,26 @@ DW_CFA_def_cfa_offset_sf = 0x13
|
||||
DW_CFA_val_offset = 0x14
|
||||
DW_CFA_val_offset_sf = 0x15
|
||||
DW_CFA_val_expression = 0x16
|
||||
DW_CFA_low_user = 0x1c
|
||||
DW_CFA_MIPS_advance_loc8 = 0x1d
|
||||
DW_CFA_GNU_window_save = 0x2d
|
||||
DW_CFA_GNU_args_size = 0x2e
|
||||
DW_CFA_GNU_negative_offset_extended = 0x2f
|
||||
DW_CFA_high_user = 0x3f
|
||||
DW_CFA_low_user = 0x1C
|
||||
DW_CFA_MIPS_advance_loc8 = 0x1D
|
||||
DW_CFA_GNU_window_save = 0x2D
|
||||
DW_CFA_GNU_args_size = 0x2E
|
||||
DW_CFA_GNU_negative_offset_extended = 0x2F
|
||||
DW_CFA_high_user = 0x3F
|
||||
|
||||
DW_CIE_ID_32 = 0xffffffff
|
||||
DW_CIE_ID_64 = 0xffffffffffffffff
|
||||
DW_CIE_ID_32 = 0xFFFFFFFF
|
||||
DW_CIE_ID_64 = 0xFFFFFFFFFFFFFFFF
|
||||
|
||||
DW_EH_PE_absptr = 0x00
|
||||
DW_EH_PE_omit = 0xff
|
||||
DW_EH_PE_omit = 0xFF
|
||||
DW_EH_PE_uleb128 = 0x01
|
||||
DW_EH_PE_udata2 = 0x02
|
||||
DW_EH_PE_udata4 = 0x03
|
||||
DW_EH_PE_udata8 = 0x04
|
||||
DW_EH_PE_sleb128 = 0x09
|
||||
DW_EH_PE_sdata2 = 0x0a
|
||||
DW_EH_PE_sdata4 = 0x0b
|
||||
DW_EH_PE_sdata8 = 0x0c
|
||||
DW_EH_PE_sdata2 = 0x0A
|
||||
DW_EH_PE_sdata4 = 0x0B
|
||||
DW_EH_PE_sdata8 = 0x0C
|
||||
DW_EH_PE_signed = 0x08
|
||||
DW_EH_PE_pcrel = 0x10
|
||||
DW_EH_PE_textrel = 0x20
|
||||
@ -26,8 +26,10 @@ def section(name: str):
|
||||
def wrapper(fn):
|
||||
fn._section = name
|
||||
return fn
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
# from types import SimpleNamespace
|
||||
|
||||
# syscalls = SimpleNamespace(
|
||||
|
||||
14
pythonbpf/expr/__init__.py
Normal file
14
pythonbpf/expr/__init__.py
Normal file
@ -0,0 +1,14 @@
|
||||
from .expr_pass import eval_expr, handle_expr, get_operand_value
|
||||
from .type_normalization import convert_to_bool, get_base_type_and_depth
|
||||
from .ir_ops import deref_to_depth
|
||||
from .call_registry import CallHandlerRegistry
|
||||
|
||||
__all__ = [
|
||||
"eval_expr",
|
||||
"handle_expr",
|
||||
"convert_to_bool",
|
||||
"get_base_type_and_depth",
|
||||
"deref_to_depth",
|
||||
"get_operand_value",
|
||||
"CallHandlerRegistry",
|
||||
]
|
||||
20
pythonbpf/expr/call_registry.py
Normal file
20
pythonbpf/expr/call_registry.py
Normal file
@ -0,0 +1,20 @@
|
||||
class CallHandlerRegistry:
|
||||
"""Registry for handling different types of calls (helpers, etc.)"""
|
||||
|
||||
_handler = None
|
||||
|
||||
@classmethod
|
||||
def set_handler(cls, handler):
|
||||
"""Set the handler for unknown calls"""
|
||||
cls._handler = handler
|
||||
|
||||
@classmethod
|
||||
def handle_call(
|
||||
cls, call, module, builder, func, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
):
|
||||
"""Handle a call using the registered handler"""
|
||||
if cls._handler is None:
|
||||
return None
|
||||
return cls._handler(
|
||||
call, module, builder, func, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
)
|
||||
566
pythonbpf/expr/expr_pass.py
Normal file
566
pythonbpf/expr/expr_pass.py
Normal file
@ -0,0 +1,566 @@
|
||||
import ast
|
||||
from llvmlite import ir
|
||||
from logging import Logger
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from pythonbpf.type_deducer import ctypes_to_ir, is_ctypes
|
||||
from .call_registry import CallHandlerRegistry
|
||||
from .type_normalization import (
|
||||
convert_to_bool,
|
||||
handle_comparator,
|
||||
get_base_type_and_depth,
|
||||
deref_to_depth,
|
||||
)
|
||||
|
||||
logger: Logger = logging.getLogger(__name__)
|
||||
|
||||
# ============================================================================
|
||||
# Leaf Handlers (No Recursive eval_expr calls)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def _handle_name_expr(expr: ast.Name, local_sym_tab: Dict, builder: ir.IRBuilder):
|
||||
"""Handle ast.Name expressions."""
|
||||
if expr.id in local_sym_tab:
|
||||
var = local_sym_tab[expr.id].var
|
||||
val = builder.load(var)
|
||||
return val, local_sym_tab[expr.id].ir_type
|
||||
else:
|
||||
logger.info(f"Undefined variable {expr.id}")
|
||||
return None
|
||||
|
||||
|
||||
def _handle_constant_expr(module, builder, expr: ast.Constant):
|
||||
"""Handle ast.Constant expressions."""
|
||||
if isinstance(expr.value, int) or isinstance(expr.value, bool):
|
||||
return ir.Constant(ir.IntType(64), int(expr.value)), ir.IntType(64)
|
||||
elif isinstance(expr.value, str):
|
||||
str_name = f".str.{id(expr)}"
|
||||
str_bytes = expr.value.encode("utf-8") + b"\x00"
|
||||
str_type = ir.ArrayType(ir.IntType(8), len(str_bytes))
|
||||
str_constant = ir.Constant(str_type, bytearray(str_bytes))
|
||||
|
||||
# Create global variable
|
||||
global_str = ir.GlobalVariable(module, str_type, name=str_name)
|
||||
global_str.linkage = "internal"
|
||||
global_str.global_constant = True
|
||||
global_str.initializer = str_constant
|
||||
|
||||
str_ptr = builder.bitcast(global_str, ir.PointerType(ir.IntType(8)))
|
||||
return str_ptr, ir.PointerType(ir.IntType(8))
|
||||
else:
|
||||
logger.error(f"Unsupported constant type {ast.dump(expr)}")
|
||||
return None
|
||||
|
||||
|
||||
def _handle_attribute_expr(
|
||||
expr: ast.Attribute,
|
||||
local_sym_tab: Dict,
|
||||
structs_sym_tab: Dict,
|
||||
builder: ir.IRBuilder,
|
||||
):
|
||||
"""Handle ast.Attribute expressions for struct field access."""
|
||||
if isinstance(expr.value, ast.Name):
|
||||
var_name = expr.value.id
|
||||
attr_name = expr.attr
|
||||
if var_name in local_sym_tab:
|
||||
var_ptr, var_type, var_metadata = local_sym_tab[var_name]
|
||||
logger.info(f"Loading attribute {attr_name} from variable {var_name}")
|
||||
logger.info(f"Variable type: {var_type}, Variable ptr: {var_ptr}")
|
||||
metadata = structs_sym_tab[var_metadata]
|
||||
if attr_name in metadata.fields:
|
||||
gep = metadata.gep(builder, var_ptr, attr_name)
|
||||
val = builder.load(gep)
|
||||
field_type = metadata.field_type(attr_name)
|
||||
return val, field_type
|
||||
return None
|
||||
|
||||
|
||||
def _handle_deref_call(expr: ast.Call, local_sym_tab: Dict, builder: ir.IRBuilder):
|
||||
"""Handle deref function calls."""
|
||||
logger.info(f"Handling deref {ast.dump(expr)}")
|
||||
if len(expr.args) != 1:
|
||||
logger.info("deref takes exactly one argument")
|
||||
return None
|
||||
|
||||
arg = expr.args[0]
|
||||
if (
|
||||
isinstance(arg, ast.Call)
|
||||
and isinstance(arg.func, ast.Name)
|
||||
and arg.func.id == "deref"
|
||||
):
|
||||
logger.info("Multiple deref not supported")
|
||||
return None
|
||||
|
||||
if isinstance(arg, ast.Name):
|
||||
if arg.id in local_sym_tab:
|
||||
arg_ptr = local_sym_tab[arg.id].var
|
||||
else:
|
||||
logger.info(f"Undefined variable {arg.id}")
|
||||
return None
|
||||
else:
|
||||
logger.info("Unsupported argument type for deref")
|
||||
return None
|
||||
|
||||
if arg_ptr is None:
|
||||
logger.info("Failed to evaluate deref argument")
|
||||
return None
|
||||
|
||||
# Load the value from pointer
|
||||
val = builder.load(arg_ptr)
|
||||
return val, local_sym_tab[arg.id].ir_type
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Binary Operations
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_operand_value(
|
||||
func, module, operand, builder, local_sym_tab, map_sym_tab, structs_sym_tab=None
|
||||
):
|
||||
"""Extract the value from an operand, handling variables and constants."""
|
||||
logger.info(f"Getting operand value for: {ast.dump(operand)}")
|
||||
if isinstance(operand, ast.Name):
|
||||
if operand.id in local_sym_tab:
|
||||
var = local_sym_tab[operand.id].var
|
||||
var_type = var.type
|
||||
base_type, depth = get_base_type_and_depth(var_type)
|
||||
logger.info(f"var is {var}, base_type is {base_type}, depth is {depth}")
|
||||
val = deref_to_depth(func, builder, var, depth)
|
||||
return val
|
||||
raise ValueError(f"Undefined variable: {operand.id}")
|
||||
elif isinstance(operand, ast.Constant):
|
||||
if isinstance(operand.value, int):
|
||||
cst = ir.Constant(ir.IntType(64), int(operand.value))
|
||||
return cst
|
||||
raise TypeError(f"Unsupported constant type: {type(operand.value)}")
|
||||
elif isinstance(operand, ast.BinOp):
|
||||
res = _handle_binary_op_impl(
|
||||
func, module, operand, builder, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
)
|
||||
return res
|
||||
else:
|
||||
res = eval_expr(
|
||||
func, module, builder, operand, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
)
|
||||
if res is None:
|
||||
raise ValueError(f"Failed to evaluate call expression: {operand}")
|
||||
val, _ = res
|
||||
logger.info(f"Evaluated expr to {val} of type {val.type}")
|
||||
base_type, depth = get_base_type_and_depth(val.type)
|
||||
if depth > 0:
|
||||
val = deref_to_depth(func, builder, val, depth)
|
||||
return val
|
||||
raise TypeError(f"Unsupported operand type: {type(operand)}")
|
||||
|
||||
|
||||
def _handle_binary_op_impl(
|
||||
func, module, rval, builder, local_sym_tab, map_sym_tab, structs_sym_tab=None
|
||||
):
|
||||
op = rval.op
|
||||
left = get_operand_value(
|
||||
func, module, rval.left, builder, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
)
|
||||
right = get_operand_value(
|
||||
func, module, rval.right, builder, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
)
|
||||
logger.info(f"left is {left}, right is {right}, op is {op}")
|
||||
|
||||
# NOTE: Before doing the operation, if the operands are integers
|
||||
# we always extend them to i64. The assignment to LHS will take
|
||||
# care of truncation if needed.
|
||||
if isinstance(left.type, ir.IntType) and left.type.width < 64:
|
||||
left = builder.sext(left, ir.IntType(64))
|
||||
if isinstance(right.type, ir.IntType) and right.type.width < 64:
|
||||
right = builder.sext(right, ir.IntType(64))
|
||||
|
||||
# Map AST operation nodes to LLVM IR builder methods
|
||||
op_map = {
|
||||
ast.Add: builder.add,
|
||||
ast.Sub: builder.sub,
|
||||
ast.Mult: builder.mul,
|
||||
ast.Div: builder.sdiv,
|
||||
ast.Mod: builder.srem,
|
||||
ast.LShift: builder.shl,
|
||||
ast.RShift: builder.lshr,
|
||||
ast.BitOr: builder.or_,
|
||||
ast.BitXor: builder.xor,
|
||||
ast.BitAnd: builder.and_,
|
||||
ast.FloorDiv: builder.udiv,
|
||||
}
|
||||
|
||||
if type(op) in op_map:
|
||||
result = op_map[type(op)](left, right)
|
||||
return result
|
||||
else:
|
||||
raise SyntaxError("Unsupported binary operation")
|
||||
|
||||
|
||||
def _handle_binary_op(
|
||||
func,
|
||||
module,
|
||||
rval,
|
||||
builder,
|
||||
var_name,
|
||||
local_sym_tab,
|
||||
map_sym_tab,
|
||||
structs_sym_tab=None,
|
||||
):
|
||||
result = _handle_binary_op_impl(
|
||||
func, module, rval, builder, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
)
|
||||
if var_name and var_name in local_sym_tab:
|
||||
logger.info(
|
||||
f"Storing result {result} into variable {local_sym_tab[var_name].var}"
|
||||
)
|
||||
builder.store(result, local_sym_tab[var_name].var)
|
||||
return result, result.type
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Comparison and Unary Operations
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def _handle_ctypes_call(
|
||||
func,
|
||||
module,
|
||||
builder,
|
||||
expr,
|
||||
local_sym_tab,
|
||||
map_sym_tab,
|
||||
structs_sym_tab=None,
|
||||
):
|
||||
"""Handle ctypes type constructor calls."""
|
||||
if len(expr.args) != 1:
|
||||
logger.info("ctypes constructor takes exactly one argument")
|
||||
return None
|
||||
|
||||
arg = expr.args[0]
|
||||
val = eval_expr(
|
||||
func,
|
||||
module,
|
||||
builder,
|
||||
arg,
|
||||
local_sym_tab,
|
||||
map_sym_tab,
|
||||
structs_sym_tab,
|
||||
)
|
||||
if val is None:
|
||||
logger.info("Failed to evaluate argument to ctypes constructor")
|
||||
return None
|
||||
call_type = expr.func.id
|
||||
expected_type = ctypes_to_ir(call_type)
|
||||
|
||||
if val[1] != expected_type:
|
||||
# NOTE: We are only considering casting to and from int types for now
|
||||
if isinstance(val[1], ir.IntType) and isinstance(expected_type, ir.IntType):
|
||||
if val[1].width < expected_type.width:
|
||||
val = (builder.sext(val[0], expected_type), expected_type)
|
||||
else:
|
||||
val = (builder.trunc(val[0], expected_type), expected_type)
|
||||
else:
|
||||
raise ValueError(f"Type mismatch: expected {expected_type}, got {val[1]}")
|
||||
return val
|
||||
|
||||
|
||||
def _handle_compare(
|
||||
func, module, builder, cond, local_sym_tab, map_sym_tab, structs_sym_tab=None
|
||||
):
|
||||
"""Handle ast.Compare expressions."""
|
||||
|
||||
if len(cond.ops) != 1 or len(cond.comparators) != 1:
|
||||
logger.error("Only single comparisons are supported")
|
||||
return None
|
||||
lhs = eval_expr(
|
||||
func,
|
||||
module,
|
||||
builder,
|
||||
cond.left,
|
||||
local_sym_tab,
|
||||
map_sym_tab,
|
||||
structs_sym_tab,
|
||||
)
|
||||
rhs = eval_expr(
|
||||
func,
|
||||
module,
|
||||
builder,
|
||||
cond.comparators[0],
|
||||
local_sym_tab,
|
||||
map_sym_tab,
|
||||
structs_sym_tab,
|
||||
)
|
||||
|
||||
if lhs is None or rhs is None:
|
||||
logger.error("Failed to evaluate comparison operands")
|
||||
return None
|
||||
|
||||
lhs, _ = lhs
|
||||
rhs, _ = rhs
|
||||
return handle_comparator(func, builder, cond.ops[0], lhs, rhs)
|
||||
|
||||
|
||||
def _handle_unary_op(
|
||||
func,
|
||||
module,
|
||||
builder,
|
||||
expr: ast.UnaryOp,
|
||||
local_sym_tab,
|
||||
map_sym_tab,
|
||||
structs_sym_tab=None,
|
||||
):
|
||||
"""Handle ast.UnaryOp expressions."""
|
||||
if not isinstance(expr.op, ast.Not) and not isinstance(expr.op, ast.USub):
|
||||
logger.error("Only 'not' and '-' unary operators are supported")
|
||||
return None
|
||||
|
||||
operand = get_operand_value(
|
||||
func, module, expr.operand, builder, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
)
|
||||
if operand is None:
|
||||
logger.error("Failed to evaluate operand for unary operation")
|
||||
return None
|
||||
|
||||
if isinstance(expr.op, ast.Not):
|
||||
true_const = ir.Constant(ir.IntType(1), 1)
|
||||
result = builder.xor(convert_to_bool(builder, operand), true_const)
|
||||
return result, ir.IntType(1)
|
||||
elif isinstance(expr.op, ast.USub):
|
||||
# Multiply by -1
|
||||
neg_one = ir.Constant(ir.IntType(64), -1)
|
||||
result = builder.mul(operand, neg_one)
|
||||
return result, ir.IntType(64)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Boolean Operations
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def _handle_and_op(func, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab):
|
||||
"""Handle `and` boolean operations."""
|
||||
|
||||
logger.debug(f"Handling 'and' operator with {len(expr.values)} operands")
|
||||
|
||||
merge_block = func.append_basic_block(name="and.merge")
|
||||
false_block = func.append_basic_block(name="and.false")
|
||||
|
||||
incoming_values = []
|
||||
|
||||
for i, value in enumerate(expr.values):
|
||||
is_last = i == len(expr.values) - 1
|
||||
|
||||
# Evaluate current operand
|
||||
operand_result = eval_expr(
|
||||
func, None, builder, value, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
)
|
||||
if operand_result is None:
|
||||
logger.error(f"Failed to evaluate operand {i} in 'and' expression")
|
||||
return None
|
||||
|
||||
operand_val, operand_type = operand_result
|
||||
|
||||
# Convert to boolean if needed
|
||||
operand_bool = convert_to_bool(builder, operand_val)
|
||||
current_block = builder.block
|
||||
|
||||
if is_last:
|
||||
# Last operand: result is this value
|
||||
builder.branch(merge_block)
|
||||
incoming_values.append((operand_bool, current_block))
|
||||
else:
|
||||
# Not last: check if true, continue or short-circuit
|
||||
next_check = func.append_basic_block(name=f"and.check_{i + 1}")
|
||||
builder.cbranch(operand_bool, next_check, false_block)
|
||||
builder.position_at_end(next_check)
|
||||
|
||||
# False block: short-circuit with false
|
||||
builder.position_at_end(false_block)
|
||||
builder.branch(merge_block)
|
||||
false_value = ir.Constant(ir.IntType(1), 0)
|
||||
incoming_values.append((false_value, false_block))
|
||||
|
||||
# Merge block: phi node
|
||||
builder.position_at_end(merge_block)
|
||||
phi = builder.phi(ir.IntType(1), name="and.result")
|
||||
for val, block in incoming_values:
|
||||
phi.add_incoming(val, block)
|
||||
|
||||
logger.debug(f"Generated 'and' with {len(incoming_values)} incoming values")
|
||||
return phi, ir.IntType(1)
|
||||
|
||||
|
||||
def _handle_or_op(func, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab):
|
||||
"""Handle `or` boolean operations."""
|
||||
|
||||
logger.debug(f"Handling 'or' operator with {len(expr.values)} operands")
|
||||
|
||||
merge_block = func.append_basic_block(name="or.merge")
|
||||
true_block = func.append_basic_block(name="or.true")
|
||||
|
||||
incoming_values = []
|
||||
|
||||
for i, value in enumerate(expr.values):
|
||||
is_last = i == len(expr.values) - 1
|
||||
|
||||
# Evaluate current operand
|
||||
operand_result = eval_expr(
|
||||
func, None, builder, value, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
)
|
||||
if operand_result is None:
|
||||
logger.error(f"Failed to evaluate operand {i} in 'or' expression")
|
||||
return None
|
||||
|
||||
operand_val, operand_type = operand_result
|
||||
|
||||
# Convert to boolean if needed
|
||||
operand_bool = convert_to_bool(builder, operand_val)
|
||||
current_block = builder.block
|
||||
|
||||
if is_last:
|
||||
# Last operand: result is this value
|
||||
builder.branch(merge_block)
|
||||
incoming_values.append((operand_bool, current_block))
|
||||
else:
|
||||
# Not last: check if false, continue or short-circuit
|
||||
next_check = func.append_basic_block(name=f"or.check_{i + 1}")
|
||||
builder.cbranch(operand_bool, true_block, next_check)
|
||||
builder.position_at_end(next_check)
|
||||
|
||||
# True block: short-circuit with true
|
||||
builder.position_at_end(true_block)
|
||||
builder.branch(merge_block)
|
||||
true_value = ir.Constant(ir.IntType(1), 1)
|
||||
incoming_values.append((true_value, true_block))
|
||||
|
||||
# Merge block: phi node
|
||||
builder.position_at_end(merge_block)
|
||||
phi = builder.phi(ir.IntType(1), name="or.result")
|
||||
for val, block in incoming_values:
|
||||
phi.add_incoming(val, block)
|
||||
|
||||
logger.debug(f"Generated 'or' with {len(incoming_values)} incoming values")
|
||||
return phi, ir.IntType(1)
|
||||
|
||||
|
||||
def _handle_boolean_op(
|
||||
func,
|
||||
module,
|
||||
builder,
|
||||
expr: ast.BoolOp,
|
||||
local_sym_tab,
|
||||
map_sym_tab,
|
||||
structs_sym_tab=None,
|
||||
):
|
||||
"""Handle `and` and `or` boolean operations."""
|
||||
|
||||
if isinstance(expr.op, ast.And):
|
||||
return _handle_and_op(
|
||||
func, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
)
|
||||
elif isinstance(expr.op, ast.Or):
|
||||
return _handle_or_op(
|
||||
func, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
)
|
||||
else:
|
||||
logger.error(f"Unsupported boolean operator: {type(expr.op).__name__}")
|
||||
return None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Expression Dispatcher
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def eval_expr(
|
||||
func,
|
||||
module,
|
||||
builder,
|
||||
expr,
|
||||
local_sym_tab,
|
||||
map_sym_tab,
|
||||
structs_sym_tab=None,
|
||||
):
|
||||
logger.info(f"Evaluating expression: {ast.dump(expr)}")
|
||||
if isinstance(expr, ast.Name):
|
||||
return _handle_name_expr(expr, local_sym_tab, builder)
|
||||
elif isinstance(expr, ast.Constant):
|
||||
return _handle_constant_expr(module, builder, expr)
|
||||
elif isinstance(expr, ast.Call):
|
||||
if isinstance(expr.func, ast.Name) and expr.func.id == "deref":
|
||||
return _handle_deref_call(expr, local_sym_tab, builder)
|
||||
|
||||
if isinstance(expr.func, ast.Name) and is_ctypes(expr.func.id):
|
||||
return _handle_ctypes_call(
|
||||
func,
|
||||
module,
|
||||
builder,
|
||||
expr,
|
||||
local_sym_tab,
|
||||
map_sym_tab,
|
||||
structs_sym_tab,
|
||||
)
|
||||
|
||||
result = CallHandlerRegistry.handle_call(
|
||||
expr, module, builder, func, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
logger.warning(f"Unknown call: {ast.dump(expr)}")
|
||||
return None
|
||||
elif isinstance(expr, ast.Attribute):
|
||||
return _handle_attribute_expr(expr, local_sym_tab, structs_sym_tab, builder)
|
||||
elif isinstance(expr, ast.BinOp):
|
||||
return _handle_binary_op(
|
||||
func,
|
||||
module,
|
||||
expr,
|
||||
builder,
|
||||
None,
|
||||
local_sym_tab,
|
||||
map_sym_tab,
|
||||
structs_sym_tab,
|
||||
)
|
||||
elif isinstance(expr, ast.Compare):
|
||||
return _handle_compare(
|
||||
func, module, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
)
|
||||
elif isinstance(expr, ast.UnaryOp):
|
||||
return _handle_unary_op(
|
||||
func, module, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
)
|
||||
elif isinstance(expr, ast.BoolOp):
|
||||
return _handle_boolean_op(
|
||||
func, module, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
)
|
||||
logger.info("Unsupported expression evaluation")
|
||||
return None
|
||||
|
||||
|
||||
def handle_expr(
|
||||
func,
|
||||
module,
|
||||
builder,
|
||||
expr,
|
||||
local_sym_tab,
|
||||
map_sym_tab,
|
||||
structs_sym_tab,
|
||||
):
|
||||
"""Handle expression statements in the function body."""
|
||||
logger.info(f"Handling expression: {ast.dump(expr)}")
|
||||
call = expr.value
|
||||
if isinstance(call, ast.Call):
|
||||
eval_expr(
|
||||
func,
|
||||
module,
|
||||
builder,
|
||||
call,
|
||||
local_sym_tab,
|
||||
map_sym_tab,
|
||||
structs_sym_tab,
|
||||
)
|
||||
else:
|
||||
logger.info("Unsupported expression type")
|
||||
50
pythonbpf/expr/ir_ops.py
Normal file
50
pythonbpf/expr/ir_ops.py
Normal file
@ -0,0 +1,50 @@
|
||||
import logging
|
||||
from llvmlite import ir
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def deref_to_depth(func, builder, val, target_depth):
|
||||
"""Dereference a pointer to a certain depth."""
|
||||
|
||||
cur_val = val
|
||||
cur_type = val.type
|
||||
|
||||
for depth in range(target_depth):
|
||||
if not isinstance(val.type, ir.PointerType):
|
||||
logger.error("Cannot dereference further, non-pointer type")
|
||||
return None
|
||||
|
||||
# dereference with null check
|
||||
pointee_type = cur_type.pointee
|
||||
null_check_block = builder.block
|
||||
not_null_block = func.append_basic_block(name=f"deref_not_null_{depth}")
|
||||
merge_block = func.append_basic_block(name=f"deref_merge_{depth}")
|
||||
|
||||
null_ptr = ir.Constant(cur_type, None)
|
||||
is_not_null = builder.icmp_signed("!=", cur_val, null_ptr)
|
||||
logger.debug(f"Inserted null check for pointer at depth {depth}")
|
||||
|
||||
builder.cbranch(is_not_null, not_null_block, merge_block)
|
||||
|
||||
builder.position_at_end(not_null_block)
|
||||
dereferenced_val = builder.load(cur_val)
|
||||
logger.debug(f"Dereferenced to depth {depth - 1}, type: {pointee_type}")
|
||||
builder.branch(merge_block)
|
||||
|
||||
builder.position_at_end(merge_block)
|
||||
phi = builder.phi(pointee_type, name=f"deref_result_{depth}")
|
||||
|
||||
zero_value = (
|
||||
ir.Constant(pointee_type, 0)
|
||||
if isinstance(pointee_type, ir.IntType)
|
||||
else ir.Constant(pointee_type, None)
|
||||
)
|
||||
phi.add_incoming(zero_value, null_check_block)
|
||||
|
||||
phi.add_incoming(dereferenced_val, not_null_block)
|
||||
|
||||
# Continue with phi result
|
||||
cur_val = phi
|
||||
cur_type = pointee_type
|
||||
return cur_val
|
||||
83
pythonbpf/expr/type_normalization.py
Normal file
83
pythonbpf/expr/type_normalization.py
Normal file
@ -0,0 +1,83 @@
|
||||
import logging
|
||||
import ast
|
||||
from llvmlite import ir
|
||||
from .ir_ops import deref_to_depth
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
COMPARISON_OPS = {
|
||||
ast.Eq: "==",
|
||||
ast.NotEq: "!=",
|
||||
ast.Lt: "<",
|
||||
ast.LtE: "<=",
|
||||
ast.Gt: ">",
|
||||
ast.GtE: ">=",
|
||||
ast.Is: "==",
|
||||
ast.IsNot: "!=",
|
||||
}
|
||||
|
||||
|
||||
def get_base_type_and_depth(ir_type):
|
||||
"""Get the base type for pointer types."""
|
||||
cur_type = ir_type
|
||||
depth = 0
|
||||
while isinstance(cur_type, ir.PointerType):
|
||||
depth += 1
|
||||
cur_type = cur_type.pointee
|
||||
return cur_type, depth
|
||||
|
||||
|
||||
def _normalize_types(func, builder, lhs, rhs):
|
||||
"""Normalize types for comparison."""
|
||||
|
||||
logger.info(f"Normalizing types: {lhs.type} vs {rhs.type}")
|
||||
if isinstance(lhs.type, ir.IntType) and isinstance(rhs.type, ir.IntType):
|
||||
if lhs.type.width < rhs.type.width:
|
||||
lhs = builder.sext(lhs, rhs.type)
|
||||
else:
|
||||
rhs = builder.sext(rhs, lhs.type)
|
||||
return lhs, rhs
|
||||
elif not isinstance(lhs.type, ir.PointerType) and not isinstance(
|
||||
rhs.type, ir.PointerType
|
||||
):
|
||||
logger.error(f"Type mismatch: {lhs.type} vs {rhs.type}")
|
||||
return None, None
|
||||
else:
|
||||
lhs_base, lhs_depth = get_base_type_and_depth(lhs.type)
|
||||
rhs_base, rhs_depth = get_base_type_and_depth(rhs.type)
|
||||
if lhs_base == rhs_base:
|
||||
if lhs_depth < rhs_depth:
|
||||
rhs = deref_to_depth(func, builder, rhs, rhs_depth - lhs_depth)
|
||||
elif rhs_depth < lhs_depth:
|
||||
lhs = deref_to_depth(func, builder, lhs, lhs_depth - rhs_depth)
|
||||
return _normalize_types(func, builder, lhs, rhs)
|
||||
|
||||
|
||||
def convert_to_bool(builder, val):
|
||||
"""Convert a value to boolean."""
|
||||
if val.type == ir.IntType(1):
|
||||
return val
|
||||
if isinstance(val.type, ir.PointerType):
|
||||
zero = ir.Constant(val.type, None)
|
||||
else:
|
||||
zero = ir.Constant(val.type, 0)
|
||||
return builder.icmp_signed("!=", val, zero)
|
||||
|
||||
|
||||
def handle_comparator(func, builder, op, lhs, rhs):
|
||||
"""Handle comparison operations."""
|
||||
|
||||
if lhs.type != rhs.type:
|
||||
lhs, rhs = _normalize_types(func, builder, lhs, rhs)
|
||||
|
||||
if lhs is None or rhs is None:
|
||||
return None
|
||||
|
||||
if type(op) not in COMPARISON_OPS:
|
||||
logger.error(f"Unsupported comparison operator: {type(op)}")
|
||||
return None
|
||||
|
||||
predicate = COMPARISON_OPS[type(op)]
|
||||
result = builder.icmp_signed(predicate, lhs, rhs)
|
||||
logger.debug(f"Comparison result: {result}")
|
||||
return result, ir.IntType(1)
|
||||
@ -1,81 +0,0 @@
|
||||
import ast
|
||||
from llvmlite import ir
|
||||
|
||||
|
||||
def eval_expr(func, module, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab=None, local_var_metadata=None):
|
||||
print(f"Evaluating expression: {ast.dump(expr)}")
|
||||
if isinstance(expr, ast.Name):
|
||||
if expr.id in local_sym_tab:
|
||||
var = local_sym_tab[expr.id][0]
|
||||
val = builder.load(var)
|
||||
return val, local_sym_tab[expr.id][1] # return value and type
|
||||
else:
|
||||
print(f"Undefined variable {expr.id}")
|
||||
return None
|
||||
elif isinstance(expr, ast.Constant):
|
||||
if isinstance(expr.value, int):
|
||||
return ir.Constant(ir.IntType(64), expr.value), ir.IntType(64)
|
||||
elif isinstance(expr.value, bool):
|
||||
return ir.Constant(ir.IntType(1), int(expr.value)), ir.IntType(1)
|
||||
else:
|
||||
print("Unsupported constant type")
|
||||
return None
|
||||
elif isinstance(expr, ast.Call):
|
||||
# delayed import to avoid circular dependency
|
||||
from .bpf_helper_handler import helper_func_list, handle_helper_call
|
||||
|
||||
if isinstance(expr.func, ast.Name):
|
||||
# check deref
|
||||
if expr.func.id == "deref":
|
||||
print(f"Handling deref {ast.dump(expr)}")
|
||||
if len(expr.args) != 1:
|
||||
print("deref takes exactly one argument")
|
||||
return None
|
||||
arg = expr.args[0]
|
||||
if isinstance(arg, ast.Call) and isinstance(arg.func, ast.Name) and arg.func.id == "deref":
|
||||
print("Multiple deref not supported")
|
||||
return None
|
||||
if isinstance(arg, ast.Name):
|
||||
if arg.id in local_sym_tab:
|
||||
arg = local_sym_tab[arg.id][0]
|
||||
else:
|
||||
print(f"Undefined variable {arg.id}")
|
||||
return None
|
||||
if arg is None:
|
||||
print("Failed to evaluate deref argument")
|
||||
return None
|
||||
# Since we are handling only name case, directly take type from sym tab
|
||||
val = builder.load(arg)
|
||||
return val, local_sym_tab[expr.args[0].id][1]
|
||||
|
||||
# check for helpers
|
||||
if expr.func.id in helper_func_list:
|
||||
return handle_helper_call(
|
||||
expr, module, builder, func, local_sym_tab, map_sym_tab, structs_sym_tab, local_var_metadata)
|
||||
elif isinstance(expr.func, ast.Attribute):
|
||||
print(f"Handling method call: {ast.dump(expr.func)}")
|
||||
if isinstance(expr.func.value, ast.Call) and isinstance(expr.func.value.func, ast.Name):
|
||||
method_name = expr.func.attr
|
||||
if method_name in helper_func_list:
|
||||
return handle_helper_call(
|
||||
expr, module, builder, func, local_sym_tab, map_sym_tab, structs_sym_tab, local_var_metadata)
|
||||
elif isinstance(expr.func.value, ast.Name):
|
||||
obj_name = expr.func.value.id
|
||||
method_name = expr.func.attr
|
||||
if obj_name in map_sym_tab:
|
||||
if method_name in helper_func_list:
|
||||
return handle_helper_call(
|
||||
expr, module, builder, func, local_sym_tab, map_sym_tab, structs_sym_tab, local_var_metadata)
|
||||
print("Unsupported expression evaluation")
|
||||
return None
|
||||
|
||||
|
||||
def handle_expr(func, module, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab, local_var_metadata):
|
||||
"""Handle expression statements in the function body."""
|
||||
print(f"Handling expression: {ast.dump(expr)}")
|
||||
call = expr.value
|
||||
if isinstance(call, ast.Call):
|
||||
eval_expr(func, module, builder, call, local_sym_tab,
|
||||
map_sym_tab, structs_sym_tab, local_var_metadata)
|
||||
else:
|
||||
print("Unsupported expression type")
|
||||
3
pythonbpf/functions/__init__.py
Normal file
3
pythonbpf/functions/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .functions_pass import func_proc
|
||||
|
||||
__all__ = ["func_proc"]
|
||||
88
pythonbpf/functions/function_metadata.py
Normal file
88
pythonbpf/functions/function_metadata.py
Normal file
@ -0,0 +1,88 @@
|
||||
import ast
|
||||
|
||||
|
||||
def get_probe_string(func_node):
|
||||
"""Extract the probe string from the decorator of the function node"""
|
||||
# TODO: right now we have the whole string in the section decorator
|
||||
# But later we can implement typed tuples for tracepoints and kprobes
|
||||
# For helper functions, we return "helper"
|
||||
|
||||
for decorator in func_node.decorator_list:
|
||||
if isinstance(decorator, ast.Name) and decorator.id == "bpfglobal":
|
||||
return None
|
||||
if isinstance(decorator, ast.Call) and isinstance(decorator.func, ast.Name):
|
||||
if decorator.func.id == "section" and len(decorator.args) == 1:
|
||||
arg = decorator.args[0]
|
||||
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
|
||||
return arg.value
|
||||
return "helper"
|
||||
|
||||
|
||||
def is_global_function(func_node):
|
||||
"""Check if the function is a global"""
|
||||
for decorator in func_node.decorator_list:
|
||||
if isinstance(decorator, ast.Name) and decorator.id in (
|
||||
"map",
|
||||
"bpfglobal",
|
||||
"struct",
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def infer_return_type(func_node: ast.FunctionDef):
|
||||
if not isinstance(func_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
raise TypeError("Expected ast.FunctionDef")
|
||||
if func_node.returns is not None:
|
||||
try:
|
||||
return ast.unparse(func_node.returns)
|
||||
except Exception:
|
||||
node = func_node.returns
|
||||
if isinstance(node, ast.Name):
|
||||
return node.id
|
||||
if isinstance(node, ast.Attribute):
|
||||
return getattr(node, "attr", type(node).__name__)
|
||||
try:
|
||||
return str(node)
|
||||
except Exception:
|
||||
return type(node).__name__
|
||||
found_type = None
|
||||
|
||||
def _expr_type(e):
|
||||
if e is None:
|
||||
return "None"
|
||||
if isinstance(e, ast.Constant):
|
||||
return type(e.value).__name__
|
||||
if isinstance(e, ast.Name):
|
||||
return e.id
|
||||
if isinstance(e, ast.Call):
|
||||
f = e.func
|
||||
if isinstance(f, ast.Name):
|
||||
return f.id
|
||||
if isinstance(f, ast.Attribute):
|
||||
try:
|
||||
return ast.unparse(f)
|
||||
except Exception:
|
||||
return getattr(f, "attr", type(f).__name__)
|
||||
try:
|
||||
return ast.unparse(f)
|
||||
except Exception:
|
||||
return type(f).__name__
|
||||
if isinstance(e, ast.Attribute):
|
||||
try:
|
||||
return ast.unparse(e)
|
||||
except Exception:
|
||||
return getattr(e, "attr", type(e).__name__)
|
||||
try:
|
||||
return ast.unparse(e)
|
||||
except Exception:
|
||||
return type(e).__name__
|
||||
|
||||
for walked_node in ast.walk(func_node):
|
||||
if isinstance(walked_node, ast.Return):
|
||||
t = _expr_type(walked_node.value)
|
||||
if found_type is None:
|
||||
found_type = t
|
||||
elif found_type != t:
|
||||
raise ValueError(f"Conflicting return types: {found_type} vs {t}")
|
||||
return found_type or "None"
|
||||
456
pythonbpf/functions/functions_pass.py
Normal file
456
pythonbpf/functions/functions_pass.py
Normal file
@ -0,0 +1,456 @@
|
||||
from llvmlite import ir
|
||||
import ast
|
||||
import logging
|
||||
|
||||
from pythonbpf.helper import (
|
||||
HelperHandlerRegistry,
|
||||
reset_scratch_pool,
|
||||
)
|
||||
from pythonbpf.type_deducer import ctypes_to_ir
|
||||
from pythonbpf.expr import eval_expr, handle_expr, convert_to_bool
|
||||
from pythonbpf.assign_pass import (
|
||||
handle_variable_assignment,
|
||||
handle_struct_field_assignment,
|
||||
)
|
||||
from pythonbpf.allocation_pass import handle_assign_allocation, allocate_temp_pool
|
||||
|
||||
from .return_utils import handle_none_return, handle_xdp_return, is_xdp_name
|
||||
from .function_metadata import get_probe_string, is_global_function, infer_return_type
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SECTION 1: Memory Allocation
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def count_temps_in_call(call_node, local_sym_tab):
|
||||
"""Count the number of temporary variables needed for a function call."""
|
||||
|
||||
count = 0
|
||||
is_helper = False
|
||||
|
||||
# NOTE: We exclude print calls for now
|
||||
if isinstance(call_node.func, ast.Name):
|
||||
if (
|
||||
HelperHandlerRegistry.has_handler(call_node.func.id)
|
||||
and call_node.func.id != "print"
|
||||
):
|
||||
is_helper = True
|
||||
elif isinstance(call_node.func, ast.Attribute):
|
||||
if HelperHandlerRegistry.has_handler(call_node.func.attr):
|
||||
is_helper = True
|
||||
|
||||
if not is_helper:
|
||||
return 0
|
||||
|
||||
for arg in call_node.args:
|
||||
# NOTE: Count all non-name arguments
|
||||
# For struct fields, if it is being passed as an argument,
|
||||
# The struct object should already exist in the local_sym_tab
|
||||
if not isinstance(arg, ast.Name) and not (
|
||||
isinstance(arg, ast.Attribute) and arg.value.id in local_sym_tab
|
||||
):
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
|
||||
def handle_if_allocation(
|
||||
module, builder, stmt, func, ret_type, map_sym_tab, local_sym_tab, structs_sym_tab
|
||||
):
|
||||
"""Recursively handle allocations in if/else branches."""
|
||||
if stmt.body:
|
||||
allocate_mem(
|
||||
module,
|
||||
builder,
|
||||
stmt.body,
|
||||
func,
|
||||
ret_type,
|
||||
map_sym_tab,
|
||||
local_sym_tab,
|
||||
structs_sym_tab,
|
||||
)
|
||||
if stmt.orelse:
|
||||
allocate_mem(
|
||||
module,
|
||||
builder,
|
||||
stmt.orelse,
|
||||
func,
|
||||
ret_type,
|
||||
map_sym_tab,
|
||||
local_sym_tab,
|
||||
structs_sym_tab,
|
||||
)
|
||||
|
||||
|
||||
def allocate_mem(
|
||||
module, builder, body, func, ret_type, map_sym_tab, local_sym_tab, structs_sym_tab
|
||||
):
|
||||
max_temps_needed = 0
|
||||
|
||||
def update_max_temps_for_stmt(stmt):
|
||||
nonlocal max_temps_needed
|
||||
temps_needed = 0
|
||||
|
||||
if isinstance(stmt, ast.If):
|
||||
for s in stmt.body:
|
||||
update_max_temps_for_stmt(s)
|
||||
for s in stmt.orelse:
|
||||
update_max_temps_for_stmt(s)
|
||||
return
|
||||
|
||||
for node in ast.walk(stmt):
|
||||
if isinstance(node, ast.Call):
|
||||
temps_needed += count_temps_in_call(node, local_sym_tab)
|
||||
max_temps_needed = max(max_temps_needed, temps_needed)
|
||||
|
||||
for stmt in body:
|
||||
update_max_temps_for_stmt(stmt)
|
||||
|
||||
# Handle allocations
|
||||
if isinstance(stmt, ast.If):
|
||||
handle_if_allocation(
|
||||
module,
|
||||
builder,
|
||||
stmt,
|
||||
func,
|
||||
ret_type,
|
||||
map_sym_tab,
|
||||
local_sym_tab,
|
||||
structs_sym_tab,
|
||||
)
|
||||
elif isinstance(stmt, ast.Assign):
|
||||
handle_assign_allocation(builder, stmt, local_sym_tab, structs_sym_tab)
|
||||
|
||||
allocate_temp_pool(builder, max_temps_needed, local_sym_tab)
|
||||
|
||||
return local_sym_tab
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SECTION 2: Statement Handlers
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def handle_assign(
|
||||
func, module, builder, stmt, map_sym_tab, local_sym_tab, structs_sym_tab
|
||||
):
|
||||
"""Handle assignment statements in the function body."""
|
||||
|
||||
# TODO: Support this later
|
||||
# GH #37
|
||||
if len(stmt.targets) != 1:
|
||||
logger.error("Multi-target assignment is not supported for now")
|
||||
return
|
||||
|
||||
target = stmt.targets[0]
|
||||
rval = stmt.value
|
||||
|
||||
if isinstance(target, ast.Name):
|
||||
# NOTE: Simple variable assignment case: x = 5
|
||||
var_name = target.id
|
||||
result = handle_variable_assignment(
|
||||
func,
|
||||
module,
|
||||
builder,
|
||||
var_name,
|
||||
rval,
|
||||
local_sym_tab,
|
||||
map_sym_tab,
|
||||
structs_sym_tab,
|
||||
)
|
||||
if not result:
|
||||
logger.error(f"Failed to handle assignment to {var_name}")
|
||||
return
|
||||
|
||||
if isinstance(target, ast.Attribute):
|
||||
# NOTE: Struct field assignment case: pkt.field = value
|
||||
handle_struct_field_assignment(
|
||||
func,
|
||||
module,
|
||||
builder,
|
||||
target,
|
||||
rval,
|
||||
local_sym_tab,
|
||||
map_sym_tab,
|
||||
structs_sym_tab,
|
||||
)
|
||||
return
|
||||
|
||||
# Unsupported target type
|
||||
logger.error(f"Unsupported assignment target: {ast.dump(target)}")
|
||||
|
||||
|
||||
def handle_cond(
|
||||
func, module, builder, cond, local_sym_tab, map_sym_tab, structs_sym_tab=None
|
||||
):
|
||||
val = eval_expr(
|
||||
func, module, builder, cond, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
)[0]
|
||||
return convert_to_bool(builder, val)
|
||||
|
||||
|
||||
def handle_if(
|
||||
func, module, builder, stmt, map_sym_tab, local_sym_tab, structs_sym_tab=None
|
||||
):
|
||||
"""Handle if statements in the function body."""
|
||||
logger.info("Handling if statement")
|
||||
# start = builder.block.parent
|
||||
then_block = func.append_basic_block(name="if.then")
|
||||
merge_block = func.append_basic_block(name="if.end")
|
||||
if stmt.orelse:
|
||||
else_block = func.append_basic_block(name="if.else")
|
||||
else:
|
||||
else_block = None
|
||||
|
||||
cond = handle_cond(
|
||||
func, module, builder, stmt.test, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
)
|
||||
if else_block:
|
||||
builder.cbranch(cond, then_block, else_block)
|
||||
else:
|
||||
builder.cbranch(cond, then_block, merge_block)
|
||||
|
||||
builder.position_at_end(then_block)
|
||||
for s in stmt.body:
|
||||
process_stmt(
|
||||
func, module, builder, s, local_sym_tab, map_sym_tab, structs_sym_tab, False
|
||||
)
|
||||
if not builder.block.is_terminated:
|
||||
builder.branch(merge_block)
|
||||
|
||||
if else_block:
|
||||
builder.position_at_end(else_block)
|
||||
for s in stmt.orelse:
|
||||
process_stmt(
|
||||
func,
|
||||
module,
|
||||
builder,
|
||||
s,
|
||||
local_sym_tab,
|
||||
map_sym_tab,
|
||||
structs_sym_tab,
|
||||
False,
|
||||
)
|
||||
if not builder.block.is_terminated:
|
||||
builder.branch(merge_block)
|
||||
|
||||
builder.position_at_end(merge_block)
|
||||
|
||||
|
||||
def handle_return(builder, stmt, local_sym_tab, ret_type):
|
||||
logger.info(f"Handling return statement: {ast.dump(stmt)}")
|
||||
if stmt.value is None:
|
||||
return handle_none_return(builder)
|
||||
elif isinstance(stmt.value, ast.Name) and is_xdp_name(stmt.value.id):
|
||||
return handle_xdp_return(stmt, builder, ret_type)
|
||||
else:
|
||||
val = eval_expr(
|
||||
func=None,
|
||||
module=None,
|
||||
builder=builder,
|
||||
expr=stmt.value,
|
||||
local_sym_tab=local_sym_tab,
|
||||
map_sym_tab={},
|
||||
structs_sym_tab={},
|
||||
)
|
||||
logger.info(f"Evaluated return expression to {val}")
|
||||
builder.ret(val[0])
|
||||
return True
|
||||
|
||||
|
||||
def process_stmt(
|
||||
func,
|
||||
module,
|
||||
builder,
|
||||
stmt,
|
||||
local_sym_tab,
|
||||
map_sym_tab,
|
||||
structs_sym_tab,
|
||||
did_return,
|
||||
ret_type=ir.IntType(64),
|
||||
):
|
||||
logger.info(f"Processing statement: {ast.dump(stmt)}")
|
||||
reset_scratch_pool()
|
||||
if isinstance(stmt, ast.Expr):
|
||||
handle_expr(
|
||||
func,
|
||||
module,
|
||||
builder,
|
||||
stmt,
|
||||
local_sym_tab,
|
||||
map_sym_tab,
|
||||
structs_sym_tab,
|
||||
)
|
||||
elif isinstance(stmt, ast.Assign):
|
||||
handle_assign(
|
||||
func, module, builder, stmt, map_sym_tab, local_sym_tab, structs_sym_tab
|
||||
)
|
||||
elif isinstance(stmt, ast.AugAssign):
|
||||
raise SyntaxError("Augmented assignment not supported")
|
||||
elif isinstance(stmt, ast.If):
|
||||
handle_if(
|
||||
func, module, builder, stmt, map_sym_tab, local_sym_tab, structs_sym_tab
|
||||
)
|
||||
elif isinstance(stmt, ast.Return):
|
||||
did_return = handle_return(
|
||||
builder,
|
||||
stmt,
|
||||
local_sym_tab,
|
||||
ret_type,
|
||||
)
|
||||
return did_return
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SECTION 3: Function Body Processing
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def process_func_body(
|
||||
module, builder, func_node, func, ret_type, map_sym_tab, structs_sym_tab
|
||||
):
|
||||
"""Process the body of a bpf function"""
|
||||
# TODO: A lot. We just have print -> bpf_trace_printk for now
|
||||
did_return = False
|
||||
|
||||
local_sym_tab = {}
|
||||
|
||||
# pre-allocate dynamic variables
|
||||
local_sym_tab = allocate_mem(
|
||||
module,
|
||||
builder,
|
||||
func_node.body,
|
||||
func,
|
||||
ret_type,
|
||||
map_sym_tab,
|
||||
local_sym_tab,
|
||||
structs_sym_tab,
|
||||
)
|
||||
|
||||
logger.info(f"Local symbol table: {local_sym_tab.keys()}")
|
||||
|
||||
for stmt in func_node.body:
|
||||
did_return = process_stmt(
|
||||
func,
|
||||
module,
|
||||
builder,
|
||||
stmt,
|
||||
local_sym_tab,
|
||||
map_sym_tab,
|
||||
structs_sym_tab,
|
||||
did_return,
|
||||
ret_type,
|
||||
)
|
||||
|
||||
if not did_return:
|
||||
builder.ret(ir.Constant(ir.IntType(64), 0))
|
||||
|
||||
|
||||
def process_bpf_chunk(func_node, module, return_type, map_sym_tab, structs_sym_tab):
|
||||
"""Process a single BPF chunk (function) and emit corresponding LLVM IR."""
|
||||
|
||||
func_name = func_node.name
|
||||
|
||||
ret_type = return_type
|
||||
|
||||
# TODO: parse parameters
|
||||
param_types = []
|
||||
if func_node.args.args:
|
||||
# Assume first arg to be ctx
|
||||
param_types.append(ir.PointerType())
|
||||
|
||||
func_ty = ir.FunctionType(ret_type, param_types)
|
||||
func = ir.Function(module, func_ty, func_name)
|
||||
|
||||
func.linkage = "dso_local"
|
||||
func.attributes.add("nounwind")
|
||||
func.attributes.add("noinline")
|
||||
func.attributes.add("optnone")
|
||||
|
||||
if func_node.args.args:
|
||||
# Only look at the first argument for now
|
||||
param = func.args[0]
|
||||
param.add_attribute("nocapture")
|
||||
|
||||
probe_string = get_probe_string(func_node)
|
||||
if probe_string is not None:
|
||||
func.section = probe_string
|
||||
|
||||
block = func.append_basic_block(name="entry")
|
||||
builder = ir.IRBuilder(block)
|
||||
|
||||
process_func_body(
|
||||
module, builder, func_node, func, ret_type, map_sym_tab, structs_sym_tab
|
||||
)
|
||||
return func
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SECTION 4: Top-Level Function Processor
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def func_proc(tree, module, chunks, map_sym_tab, structs_sym_tab):
|
||||
for func_node in chunks:
|
||||
if is_global_function(func_node):
|
||||
continue
|
||||
func_type = get_probe_string(func_node)
|
||||
logger.info(f"Found probe_string of {func_node.name}: {func_type}")
|
||||
|
||||
process_bpf_chunk(
|
||||
func_node,
|
||||
module,
|
||||
ctypes_to_ir(infer_return_type(func_node)),
|
||||
map_sym_tab,
|
||||
structs_sym_tab,
|
||||
)
|
||||
|
||||
|
||||
# TODO: WIP, for string assignment to fixed-size arrays
|
||||
def assign_string_to_array(builder, target_array_ptr, source_string_ptr, array_length):
|
||||
"""
|
||||
Copy a string (i8*) to a fixed-size array ([N x i8]*)
|
||||
"""
|
||||
# Create a loop to copy characters one by one
|
||||
# entry_block = builder.block
|
||||
copy_block = builder.append_basic_block("copy_char")
|
||||
end_block = builder.append_basic_block("copy_end")
|
||||
|
||||
# Create loop counter
|
||||
i = builder.alloca(ir.IntType(32))
|
||||
builder.store(ir.Constant(ir.IntType(32), 0), i)
|
||||
|
||||
# Start the loop
|
||||
builder.branch(copy_block)
|
||||
|
||||
# Copy loop
|
||||
builder.position_at_end(copy_block)
|
||||
idx = builder.load(i)
|
||||
in_bounds = builder.icmp_unsigned(
|
||||
"<", idx, ir.Constant(ir.IntType(32), array_length)
|
||||
)
|
||||
builder.cbranch(in_bounds, copy_block, end_block)
|
||||
|
||||
with builder.if_then(in_bounds):
|
||||
# Load character from source
|
||||
src_ptr = builder.gep(source_string_ptr, [idx])
|
||||
char = builder.load(src_ptr)
|
||||
|
||||
# Store character in target
|
||||
dst_ptr = builder.gep(target_array_ptr, [ir.Constant(ir.IntType(32), 0), idx])
|
||||
builder.store(char, dst_ptr)
|
||||
|
||||
# Increment counter
|
||||
next_idx = builder.add(idx, ir.Constant(ir.IntType(32), 1))
|
||||
builder.store(next_idx, i)
|
||||
|
||||
builder.position_at_end(end_block)
|
||||
|
||||
# Ensure null termination
|
||||
last_idx = ir.Constant(ir.IntType(32), array_length - 1)
|
||||
null_ptr = builder.gep(target_array_ptr, [ir.Constant(ir.IntType(32), 0), last_idx])
|
||||
builder.store(ir.Constant(ir.IntType(8), 0), null_ptr)
|
||||
44
pythonbpf/functions/return_utils.py
Normal file
44
pythonbpf/functions/return_utils.py
Normal file
@ -0,0 +1,44 @@
|
||||
import logging
|
||||
import ast
|
||||
|
||||
from llvmlite import ir
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
XDP_ACTIONS = {
|
||||
"XDP_ABORTED": 0,
|
||||
"XDP_DROP": 1,
|
||||
"XDP_PASS": 2,
|
||||
"XDP_TX": 3,
|
||||
"XDP_REDIRECT": 4,
|
||||
}
|
||||
|
||||
|
||||
def handle_none_return(builder) -> bool:
|
||||
"""Handle return or return None -> returns 0."""
|
||||
builder.ret(ir.Constant(ir.IntType(64), 0))
|
||||
logger.debug("Generated default return: 0")
|
||||
return True
|
||||
|
||||
|
||||
def is_xdp_name(name: str) -> bool:
|
||||
"""Check if a name is an XDP action"""
|
||||
return name in XDP_ACTIONS
|
||||
|
||||
|
||||
def handle_xdp_return(stmt: ast.Return, builder, ret_type) -> bool:
|
||||
"""Handle XDP returns"""
|
||||
if not isinstance(stmt.value, ast.Name):
|
||||
return False
|
||||
|
||||
action_name = stmt.value.id
|
||||
|
||||
if action_name not in XDP_ACTIONS:
|
||||
raise ValueError(
|
||||
f"Unknown XDP action: {action_name}. Available: {XDP_ACTIONS.keys()}"
|
||||
)
|
||||
|
||||
value = XDP_ACTIONS[action_name]
|
||||
builder.ret(ir.Constant(ret_type, value))
|
||||
logger.debug(f"Generated XDP action return: {action_name} = {value}")
|
||||
return True
|
||||
@ -1,590 +0,0 @@
|
||||
from llvmlite import ir
|
||||
import ast
|
||||
|
||||
|
||||
from .bpf_helper_handler import helper_func_list, handle_helper_call
|
||||
from .type_deducer import ctypes_to_ir
|
||||
from .binary_ops import handle_binary_op
|
||||
from .expr_pass import eval_expr, handle_expr
|
||||
|
||||
local_var_metadata = {}
|
||||
|
||||
|
||||
def get_probe_string(func_node):
|
||||
"""Extract the probe string from the decorator of the function node."""
|
||||
# TODO: right now we have the whole string in the section decorator
|
||||
# But later we can implement typed tuples for tracepoints and kprobes
|
||||
# For helper functions, we return "helper"
|
||||
|
||||
for decorator in func_node.decorator_list:
|
||||
if isinstance(decorator, ast.Name) and decorator.id == "bpfglobal":
|
||||
return None
|
||||
if isinstance(decorator, ast.Call) and isinstance(decorator.func, ast.Name):
|
||||
if decorator.func.id == "section" and len(decorator.args) == 1:
|
||||
arg = decorator.args[0]
|
||||
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
|
||||
return arg.value
|
||||
return "helper"
|
||||
|
||||
|
||||
def handle_assign(func, module, builder, stmt, map_sym_tab, local_sym_tab, structs_sym_tab):
|
||||
"""Handle assignment statements in the function body."""
|
||||
if len(stmt.targets) != 1:
|
||||
print("Unsupported multiassignment")
|
||||
return
|
||||
|
||||
num_types = ("c_int32", "c_int64", "c_uint32", "c_uint64")
|
||||
|
||||
target = stmt.targets[0]
|
||||
print(f"Handling assignment to {ast.dump(target)}")
|
||||
if not isinstance(target, ast.Name) and not isinstance(target, ast.Attribute):
|
||||
print("Unsupported assignment target")
|
||||
return
|
||||
var_name = target.id if isinstance(target, ast.Name) else target.value.id
|
||||
rval = stmt.value
|
||||
if isinstance(target, ast.Attribute):
|
||||
# struct field assignment
|
||||
field_name = target.attr
|
||||
if var_name in local_sym_tab and var_name in local_var_metadata:
|
||||
struct_type = local_var_metadata[var_name]
|
||||
struct_info = structs_sym_tab[struct_type]
|
||||
|
||||
if field_name in struct_info["fields"]:
|
||||
field_idx = struct_info["fields"][field_name]
|
||||
struct_ptr = local_sym_tab[var_name][0]
|
||||
field_ptr = builder.gep(
|
||||
struct_ptr, [ir.Constant(ir.IntType(32), 0),
|
||||
ir.Constant(ir.IntType(32), field_idx)],
|
||||
inbounds=True)
|
||||
val = eval_expr(func, module, builder, rval,
|
||||
local_sym_tab, map_sym_tab, structs_sym_tab)
|
||||
if isinstance(struct_info["field_types"][field_idx], ir.ArrayType) and val[1] == ir.PointerType(ir.IntType(8)):
|
||||
# TODO: Figure it out, not a priority rn
|
||||
# Special case for string assignment to char array
|
||||
#str_len = struct_info["field_types"][field_idx].count
|
||||
#assign_string_to_array(builder, field_ptr, val[0], str_len)
|
||||
#print(f"Assigned to struct field {var_name}.{field_name}")
|
||||
pass
|
||||
if val is None:
|
||||
print("Failed to evaluate struct field assignment")
|
||||
return
|
||||
print(field_ptr)
|
||||
builder.store(val[0], field_ptr)
|
||||
print(f"Assigned to struct field {var_name}.{field_name}")
|
||||
return
|
||||
elif isinstance(rval, ast.Constant):
|
||||
if isinstance(rval.value, bool):
|
||||
if rval.value:
|
||||
builder.store(ir.Constant(ir.IntType(1), 1),
|
||||
local_sym_tab[var_name][0])
|
||||
else:
|
||||
builder.store(ir.Constant(ir.IntType(1), 0),
|
||||
local_sym_tab[var_name][0])
|
||||
print(f"Assigned constant {rval.value} to {var_name}")
|
||||
elif isinstance(rval.value, int):
|
||||
# Assume c_int64 for now
|
||||
# var = builder.alloca(ir.IntType(64), name=var_name)
|
||||
# var.align = 8
|
||||
builder.store(ir.Constant(ir.IntType(64), rval.value),
|
||||
local_sym_tab[var_name][0])
|
||||
# local_sym_tab[var_name] = var
|
||||
print(f"Assigned constant {rval.value} to {var_name}")
|
||||
elif isinstance(rval.value, str):
|
||||
str_val = rval.value.encode('utf-8') + b'\x00'
|
||||
str_const = ir.Constant(ir.ArrayType(
|
||||
ir.IntType(8), len(str_val)), bytearray(str_val))
|
||||
global_str = ir.GlobalVariable(
|
||||
module, str_const.type, name=f"{var_name}_str")
|
||||
global_str.linkage = 'internal'
|
||||
global_str.global_constant = True
|
||||
global_str.initializer = str_const
|
||||
str_ptr = builder.bitcast(
|
||||
global_str, ir.PointerType(ir.IntType(8)))
|
||||
builder.store(str_ptr, local_sym_tab[var_name][0])
|
||||
print(f"Assigned string constant '{rval.value}' to {var_name}")
|
||||
else:
|
||||
print("Unsupported constant type")
|
||||
elif isinstance(rval, ast.Call):
|
||||
if isinstance(rval.func, ast.Name):
|
||||
call_type = rval.func.id
|
||||
print(f"Assignment call type: {call_type}")
|
||||
if call_type in num_types and len(rval.args) == 1 and isinstance(rval.args[0], ast.Constant) and isinstance(rval.args[0].value, int):
|
||||
ir_type = ctypes_to_ir(call_type)
|
||||
# var = builder.alloca(ir_type, name=var_name)
|
||||
# var.align = ir_type.width // 8
|
||||
builder.store(ir.Constant(
|
||||
ir_type, rval.args[0].value), local_sym_tab[var_name][0])
|
||||
print(f"Assigned {call_type} constant "
|
||||
f"{rval.args[0].value} to {var_name}")
|
||||
# local_sym_tab[var_name] = var
|
||||
elif call_type in helper_func_list:
|
||||
# var = builder.alloca(ir.IntType(64), name=var_name)
|
||||
# var.align = 8
|
||||
val = handle_helper_call(
|
||||
rval, module, builder, func, local_sym_tab, map_sym_tab, structs_sym_tab, local_var_metadata)
|
||||
builder.store(val[0], local_sym_tab[var_name][0])
|
||||
# local_sym_tab[var_name] = var
|
||||
print(f"Assigned constant {rval.func.id} to {var_name}")
|
||||
elif call_type == "deref" and len(rval.args) == 1:
|
||||
print(f"Handling deref assignment {ast.dump(rval)}")
|
||||
val = eval_expr(func, module, builder, rval,
|
||||
local_sym_tab, map_sym_tab, structs_sym_tab)
|
||||
if val is None:
|
||||
print("Failed to evaluate deref argument")
|
||||
return
|
||||
print(f"Dereferenced value: {val}, storing in {var_name}")
|
||||
builder.store(val[0], local_sym_tab[var_name][0])
|
||||
# local_sym_tab[var_name] = var
|
||||
print(f"Dereferenced and assigned to {var_name}")
|
||||
elif call_type in structs_sym_tab and len(rval.args) == 0:
|
||||
struct_info = structs_sym_tab[call_type]
|
||||
ir_type = struct_info["type"]
|
||||
# var = builder.alloca(ir_type, name=var_name)
|
||||
# Null init
|
||||
builder.store(ir.Constant(ir_type, None),
|
||||
local_sym_tab[var_name][0])
|
||||
local_var_metadata[var_name] = call_type
|
||||
print(f"Assigned struct {call_type} to {var_name}")
|
||||
# local_sym_tab[var_name] = var
|
||||
else:
|
||||
print(f"Unsupported assignment call type: {call_type}")
|
||||
elif isinstance(rval.func, ast.Attribute):
|
||||
print(f"Assignment call attribute: {ast.dump(rval.func)}")
|
||||
if isinstance(rval.func.value, ast.Name):
|
||||
# TODO: probably a struct access
|
||||
print(f"TODO STRUCT ACCESS {ast.dump(rval)}")
|
||||
elif isinstance(rval.func.value, ast.Call) and isinstance(rval.func.value.func, ast.Name):
|
||||
map_name = rval.func.value.func.id
|
||||
method_name = rval.func.attr
|
||||
if map_name in map_sym_tab:
|
||||
map_ptr = map_sym_tab[map_name]
|
||||
if method_name in helper_func_list:
|
||||
val = handle_helper_call(
|
||||
rval, module, builder, func, local_sym_tab, map_sym_tab, structs_sym_tab, local_var_metadata)
|
||||
# var = builder.alloca(ir.IntType(64), name=var_name)
|
||||
# var.align = 8
|
||||
builder.store(val[0], local_sym_tab[var_name][0])
|
||||
# local_sym_tab[var_name] = var
|
||||
else:
|
||||
print("Unsupported assignment call structure")
|
||||
else:
|
||||
print("Unsupported assignment call function type")
|
||||
elif isinstance(rval, ast.BinOp):
|
||||
handle_binary_op(rval, module, builder, var_name,
|
||||
local_sym_tab, map_sym_tab, func)
|
||||
else:
|
||||
print("Unsupported assignment value type")
|
||||
|
||||
|
||||
def handle_cond(func, module, builder, cond, local_sym_tab, map_sym_tab):
|
||||
if isinstance(cond, ast.Constant):
|
||||
if isinstance(cond.value, bool):
|
||||
return ir.Constant(ir.IntType(1), int(cond.value))
|
||||
elif isinstance(cond.value, int):
|
||||
return ir.Constant(ir.IntType(1), int(bool(cond.value)))
|
||||
else:
|
||||
print("Unsupported constant type in condition")
|
||||
return None
|
||||
elif isinstance(cond, ast.Name):
|
||||
if cond.id in local_sym_tab:
|
||||
var = local_sym_tab[cond.id][0]
|
||||
val = builder.load(var)
|
||||
if val.type != ir.IntType(1):
|
||||
# Convert nonzero values to true, zero to false
|
||||
if isinstance(val.type, ir.PointerType):
|
||||
# For pointer types, compare with null pointer
|
||||
zero = ir.Constant(val.type, None)
|
||||
else:
|
||||
# For integer types, compare with zero
|
||||
zero = ir.Constant(val.type, 0)
|
||||
val = builder.icmp_signed("!=", val, zero)
|
||||
return val
|
||||
else:
|
||||
print(f"Undefined variable {cond.id} in condition")
|
||||
return None
|
||||
elif isinstance(cond, ast.Compare):
|
||||
lhs = eval_expr(func, module, builder, cond.left,
|
||||
local_sym_tab, map_sym_tab)[0]
|
||||
if len(cond.ops) != 1 or len(cond.comparators) != 1:
|
||||
print("Unsupported complex comparison")
|
||||
return None
|
||||
rhs = eval_expr(func, module, builder,
|
||||
cond.comparators[0], local_sym_tab, map_sym_tab)[0]
|
||||
op = cond.ops[0]
|
||||
|
||||
if lhs.type != rhs.type:
|
||||
if isinstance(lhs.type, ir.IntType) and isinstance(rhs.type, ir.IntType):
|
||||
# Extend the smaller type to the larger type
|
||||
if lhs.type.width < rhs.type.width:
|
||||
lhs = builder.sext(lhs, rhs.type)
|
||||
elif lhs.type.width > rhs.type.width:
|
||||
rhs = builder.sext(rhs, lhs.type)
|
||||
else:
|
||||
print("Type mismatch in comparison")
|
||||
return None
|
||||
|
||||
if isinstance(op, ast.Eq):
|
||||
return builder.icmp_signed("==", lhs, rhs)
|
||||
elif isinstance(op, ast.NotEq):
|
||||
return builder.icmp_signed("!=", lhs, rhs)
|
||||
elif isinstance(op, ast.Lt):
|
||||
return builder.icmp_signed("<", lhs, rhs)
|
||||
elif isinstance(op, ast.LtE):
|
||||
return builder.icmp_signed("<=", lhs, rhs)
|
||||
elif isinstance(op, ast.Gt):
|
||||
return builder.icmp_signed(">", lhs, rhs)
|
||||
elif isinstance(op, ast.GtE):
|
||||
return builder.icmp_signed(">=", lhs, rhs)
|
||||
else:
|
||||
print("Unsupported comparison operator")
|
||||
return None
|
||||
else:
|
||||
print("Unsupported condition expression")
|
||||
return None
|
||||
|
||||
|
||||
def handle_if(func, module, builder, stmt, map_sym_tab, local_sym_tab, structs_sym_tab=None):
|
||||
"""Handle if statements in the function body."""
|
||||
print("Handling if statement")
|
||||
start = builder.block.parent
|
||||
then_block = func.append_basic_block(name="if.then")
|
||||
merge_block = func.append_basic_block(name="if.end")
|
||||
if stmt.orelse:
|
||||
else_block = func.append_basic_block(name="if.else")
|
||||
else:
|
||||
else_block = None
|
||||
|
||||
cond = handle_cond(func, module, builder, stmt.test,
|
||||
local_sym_tab, map_sym_tab)
|
||||
if else_block:
|
||||
builder.cbranch(cond, then_block, else_block)
|
||||
else:
|
||||
builder.cbranch(cond, then_block, merge_block)
|
||||
|
||||
builder.position_at_end(then_block)
|
||||
for s in stmt.body:
|
||||
process_stmt(func, module, builder, s,
|
||||
local_sym_tab, map_sym_tab, structs_sym_tab, False)
|
||||
if not builder.block.is_terminated:
|
||||
builder.branch(merge_block)
|
||||
|
||||
if else_block:
|
||||
builder.position_at_end(else_block)
|
||||
for s in stmt.orelse:
|
||||
process_stmt(func, module, builder, s,
|
||||
local_sym_tab, map_sym_tab, structs_sym_tab, False)
|
||||
if not builder.block.is_terminated:
|
||||
builder.branch(merge_block)
|
||||
|
||||
builder.position_at_end(merge_block)
|
||||
|
||||
|
||||
def process_stmt(func, module, builder, stmt, local_sym_tab, map_sym_tab, structs_sym_tab, did_return, ret_type=ir.IntType(64)):
|
||||
print(f"Processing statement: {ast.dump(stmt)}")
|
||||
if isinstance(stmt, ast.Expr):
|
||||
handle_expr(func, module, builder, stmt, local_sym_tab,
|
||||
map_sym_tab, structs_sym_tab, local_var_metadata)
|
||||
elif isinstance(stmt, ast.Assign):
|
||||
handle_assign(func, module, builder, stmt, map_sym_tab,
|
||||
local_sym_tab, structs_sym_tab)
|
||||
elif isinstance(stmt, ast.AugAssign):
|
||||
raise SyntaxError("Augmented assignment not supported")
|
||||
elif isinstance(stmt, ast.If):
|
||||
handle_if(func, module, builder, stmt, map_sym_tab,
|
||||
local_sym_tab, structs_sym_tab)
|
||||
elif isinstance(stmt, ast.Return):
|
||||
if stmt.value is None:
|
||||
builder.ret(ir.Constant(ir.IntType(32), 0))
|
||||
did_return = True
|
||||
elif isinstance(stmt.value, ast.Call) and isinstance(stmt.value.func, ast.Name) and len(stmt.value.args) == 1 and isinstance(stmt.value.args[0], ast.Constant) and isinstance(stmt.value.args[0].value, int):
|
||||
call_type = stmt.value.func.id
|
||||
if ctypes_to_ir(call_type) != ret_type:
|
||||
raise ValueError("Return type mismatch: expected"
|
||||
f"{ctypes_to_ir(call_type)}, got {call_type}")
|
||||
else:
|
||||
builder.ret(ir.Constant(
|
||||
ret_type, stmt.value.args[0].value))
|
||||
did_return = True
|
||||
elif isinstance(stmt.value, ast.Name):
|
||||
if stmt.value.id == "XDP_PASS":
|
||||
builder.ret(ir.Constant(ret_type, 2))
|
||||
did_return = True
|
||||
elif stmt.value.id == "XDP_DROP":
|
||||
builder.ret(ir.Constant(ret_type, 1))
|
||||
did_return = True
|
||||
else:
|
||||
raise ValueError("Failed to evaluate return expression")
|
||||
else:
|
||||
raise ValueError("Unsupported return value")
|
||||
return did_return
|
||||
|
||||
|
||||
def allocate_mem(module, builder, body, func, ret_type, map_sym_tab, local_sym_tab, structs_sym_tab):
|
||||
for stmt in body:
|
||||
if isinstance(stmt, ast.If):
|
||||
if stmt.body:
|
||||
local_sym_tab = allocate_mem(
|
||||
module, builder, stmt.body, func, ret_type, map_sym_tab, local_sym_tab, structs_sym_tab)
|
||||
if stmt.orelse:
|
||||
local_sym_tab = allocate_mem(
|
||||
module, builder, stmt.orelse, func, ret_type, map_sym_tab, local_sym_tab, structs_sym_tab)
|
||||
elif isinstance(stmt, ast.Assign):
|
||||
if len(stmt.targets) != 1:
|
||||
print("Unsupported multiassignment")
|
||||
continue
|
||||
target = stmt.targets[0]
|
||||
if not isinstance(target, ast.Name):
|
||||
print("Unsupported assignment target")
|
||||
continue
|
||||
var_name = target.id
|
||||
rval = stmt.value
|
||||
if isinstance(rval, ast.Call):
|
||||
if isinstance(rval.func, ast.Name):
|
||||
call_type = rval.func.id
|
||||
if call_type in ("c_int32", "c_int64", "c_uint32", "c_uint64"):
|
||||
ir_type = ctypes_to_ir(call_type)
|
||||
var = builder.alloca(ir_type, name=var_name)
|
||||
var.align = ir_type.width // 8
|
||||
print(
|
||||
f"Pre-allocated variable {var_name} of type {call_type}")
|
||||
elif call_type in helper_func_list:
|
||||
# Assume return type is int64 for now
|
||||
ir_type = ir.IntType(64)
|
||||
var = builder.alloca(ir_type, name=var_name)
|
||||
var.align = ir_type.width // 8
|
||||
print(
|
||||
f"Pre-allocated variable {var_name} for helper")
|
||||
elif call_type == "deref" and len(rval.args) == 1:
|
||||
# Assume return type is int64 for now
|
||||
ir_type = ir.IntType(64)
|
||||
var = builder.alloca(ir_type, name=var_name)
|
||||
var.align = ir_type.width // 8
|
||||
print(
|
||||
f"Pre-allocated variable {var_name} for deref")
|
||||
elif call_type in structs_sym_tab:
|
||||
struct_info = structs_sym_tab[call_type]
|
||||
ir_type = struct_info["type"]
|
||||
var = builder.alloca(ir_type, name=var_name)
|
||||
local_var_metadata[var_name] = call_type
|
||||
print(
|
||||
f"Pre-allocated variable {var_name} for struct {call_type}")
|
||||
elif isinstance(rval.func, ast.Attribute):
|
||||
ir_type = ir.PointerType(ir.IntType(64))
|
||||
var = builder.alloca(ir_type, name=var_name)
|
||||
# var.align = ir_type.width // 8
|
||||
print(
|
||||
f"Pre-allocated variable {var_name} for map")
|
||||
else:
|
||||
print("Unsupported assignment call function type")
|
||||
continue
|
||||
elif isinstance(rval, ast.Constant):
|
||||
if isinstance(rval.value, bool):
|
||||
ir_type = ir.IntType(1)
|
||||
var = builder.alloca(ir_type, name=var_name)
|
||||
var.align = 1
|
||||
print(
|
||||
f"Pre-allocated variable {var_name} of type c_bool")
|
||||
elif isinstance(rval.value, int):
|
||||
# Assume c_int64 for now
|
||||
ir_type = ir.IntType(64)
|
||||
var = builder.alloca(ir_type, name=var_name)
|
||||
var.align = ir_type.width // 8
|
||||
print(
|
||||
f"Pre-allocated variable {var_name} of type c_int64")
|
||||
elif isinstance(rval.value, str):
|
||||
ir_type = ir.PointerType(ir.IntType(8))
|
||||
var = builder.alloca(ir_type, name=var_name)
|
||||
var.align = 8
|
||||
print(
|
||||
f"Pre-allocated variable {var_name} of type string")
|
||||
else:
|
||||
print(f"Unsupported constant type")
|
||||
continue
|
||||
elif isinstance(rval, ast.BinOp):
|
||||
# Assume c_int64 for now
|
||||
ir_type = ir.IntType(64)
|
||||
var = builder.alloca(ir_type, name=var_name)
|
||||
var.align = ir_type.width // 8
|
||||
print(
|
||||
f"Pre-allocated variable {var_name} of type c_int64")
|
||||
else:
|
||||
print("Unsupported assignment value type")
|
||||
continue
|
||||
local_sym_tab[var_name] = (var, ir_type)
|
||||
return local_sym_tab
|
||||
|
||||
|
||||
def process_func_body(module, builder, func_node, func, ret_type, map_sym_tab, structs_sym_tab):
|
||||
"""Process the body of a bpf function"""
|
||||
# TODO: A lot. We just have print -> bpf_trace_printk for now
|
||||
did_return = False
|
||||
|
||||
local_sym_tab = {}
|
||||
|
||||
# pre-allocate dynamic variables
|
||||
local_sym_tab = allocate_mem(
|
||||
module, builder, func_node.body, func, ret_type, map_sym_tab, local_sym_tab, structs_sym_tab)
|
||||
|
||||
print(f"Local symbol table: {local_sym_tab.keys()}")
|
||||
|
||||
for stmt in func_node.body:
|
||||
did_return = process_stmt(func, module, builder, stmt, local_sym_tab,
|
||||
map_sym_tab, structs_sym_tab, did_return, ret_type)
|
||||
|
||||
if not did_return:
|
||||
builder.ret(ir.Constant(ir.IntType(32), 0))
|
||||
|
||||
|
||||
def process_bpf_chunk(func_node, module, return_type, map_sym_tab, structs_sym_tab):
|
||||
"""Process a single BPF chunk (function) and emit corresponding LLVM IR."""
|
||||
|
||||
func_name = func_node.name
|
||||
|
||||
ret_type = return_type
|
||||
|
||||
# TODO: parse parameters
|
||||
param_types = []
|
||||
if func_node.args.args:
|
||||
# Assume first arg to be ctx
|
||||
param_types.append(ir.PointerType())
|
||||
|
||||
func_ty = ir.FunctionType(ret_type, param_types)
|
||||
func = ir.Function(module, func_ty, func_name)
|
||||
|
||||
func.linkage = "dso_local"
|
||||
func.attributes.add("nounwind")
|
||||
func.attributes.add("noinline")
|
||||
func.attributes.add("optnone")
|
||||
|
||||
if func_node.args.args:
|
||||
# Only look at the first argument for now
|
||||
param = func.args[0]
|
||||
param.add_attribute("nocapture")
|
||||
|
||||
probe_string = get_probe_string(func_node)
|
||||
if probe_string is not None:
|
||||
func.section = probe_string
|
||||
|
||||
block = func.append_basic_block(name="entry")
|
||||
builder = ir.IRBuilder(block)
|
||||
|
||||
process_func_body(module, builder, func_node, func,
|
||||
ret_type, map_sym_tab, structs_sym_tab)
|
||||
return func
|
||||
|
||||
|
||||
def func_proc(tree, module, chunks, map_sym_tab, structs_sym_tab):
|
||||
for func_node in chunks:
|
||||
is_global = False
|
||||
for decorator in func_node.decorator_list:
|
||||
if isinstance(decorator, ast.Name) and decorator.id in ("map", "bpfglobal", "struct"):
|
||||
is_global = True
|
||||
break
|
||||
if is_global:
|
||||
continue
|
||||
func_type = get_probe_string(func_node)
|
||||
print(f"Found probe_string of {func_node.name}: {func_type}")
|
||||
|
||||
process_bpf_chunk(func_node, module, ctypes_to_ir(
|
||||
infer_return_type(func_node)), map_sym_tab, structs_sym_tab)
|
||||
|
||||
|
||||
def infer_return_type(func_node: ast.FunctionDef):
|
||||
if not isinstance(func_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
raise TypeError("Expected ast.FunctionDef")
|
||||
if func_node.returns is not None:
|
||||
try:
|
||||
return ast.unparse(func_node.returns)
|
||||
except Exception:
|
||||
node = func_node.returns
|
||||
if isinstance(node, ast.Name):
|
||||
return node.id
|
||||
if isinstance(node, ast.Attribute):
|
||||
return getattr(node, "attr", type(node).__name__)
|
||||
try:
|
||||
return str(node)
|
||||
except Exception:
|
||||
return type(node).__name__
|
||||
found_type = None
|
||||
|
||||
def _expr_type(e):
|
||||
if e is None:
|
||||
return "None"
|
||||
if isinstance(e, ast.Constant):
|
||||
return type(e.value).__name__
|
||||
if isinstance(e, ast.Name):
|
||||
return e.id
|
||||
if isinstance(e, ast.Call):
|
||||
f = e.func
|
||||
if isinstance(f, ast.Name):
|
||||
return f.id
|
||||
if isinstance(f, ast.Attribute):
|
||||
try:
|
||||
return ast.unparse(f)
|
||||
except Exception:
|
||||
return getattr(f, "attr", type(f).__name__)
|
||||
try:
|
||||
return ast.unparse(f)
|
||||
except Exception:
|
||||
return type(f).__name__
|
||||
if isinstance(e, ast.Attribute):
|
||||
try:
|
||||
return ast.unparse(e)
|
||||
except Exception:
|
||||
return getattr(e, "attr", type(e).__name__)
|
||||
try:
|
||||
return ast.unparse(e)
|
||||
except Exception:
|
||||
return type(e).__name__
|
||||
for node in ast.walk(func_node):
|
||||
if isinstance(node, ast.Return):
|
||||
t = _expr_type(node.value)
|
||||
if found_type is None:
|
||||
found_type = t
|
||||
elif found_type != t:
|
||||
raise ValueError("Conflicting return types:"
|
||||
f"{found_type} vs {t}")
|
||||
return found_type or "None"
|
||||
|
||||
# For string assignment to fixed-size arrays
|
||||
def assign_string_to_array(builder, target_array_ptr, source_string_ptr, array_length):
|
||||
"""
|
||||
Copy a string (i8*) to a fixed-size array ([N x i8]*)
|
||||
"""
|
||||
# Create a loop to copy characters one by one
|
||||
entry_block = builder.block
|
||||
copy_block = builder.append_basic_block("copy_char")
|
||||
end_block = builder.append_basic_block("copy_end")
|
||||
|
||||
# Create loop counter
|
||||
i = builder.alloca(ir.IntType(32))
|
||||
builder.store(ir.Constant(ir.IntType(32), 0), i)
|
||||
|
||||
# Start the loop
|
||||
builder.branch(copy_block)
|
||||
|
||||
# Copy loop
|
||||
builder.position_at_end(copy_block)
|
||||
idx = builder.load(i)
|
||||
in_bounds = builder.icmp_unsigned('<', idx, ir.Constant(ir.IntType(32), array_length))
|
||||
builder.cbranch(in_bounds, copy_block, end_block)
|
||||
|
||||
with builder.if_then(in_bounds):
|
||||
# Load character from source
|
||||
src_ptr = builder.gep(source_string_ptr, [idx])
|
||||
char = builder.load(src_ptr)
|
||||
|
||||
# Store character in target
|
||||
dst_ptr = builder.gep(target_array_ptr, [ir.Constant(ir.IntType(32), 0), idx])
|
||||
builder.store(char, dst_ptr)
|
||||
|
||||
# Increment counter
|
||||
next_idx = builder.add(idx, ir.Constant(ir.IntType(32), 1))
|
||||
builder.store(next_idx, i)
|
||||
|
||||
builder.position_at_end(end_block)
|
||||
|
||||
# Ensure null termination
|
||||
last_idx = ir.Constant(ir.IntType(32), array_length - 1)
|
||||
null_ptr = builder.gep(target_array_ptr, [ir.Constant(ir.IntType(32), 0), last_idx])
|
||||
builder.store(ir.Constant(ir.IntType(8), 0), null_ptr)
|
||||
@ -1,8 +1,121 @@
|
||||
from llvmlite import ir
|
||||
import ast
|
||||
|
||||
from logging import Logger
|
||||
import logging
|
||||
from .type_deducer import ctypes_to_ir
|
||||
|
||||
def emit_globals(module: ir.Module, names: list[str]):
|
||||
logger: Logger = logging.getLogger(__name__)
|
||||
|
||||
# TODO: this is going to be a huge fuck of a headache in the future.
|
||||
global_sym_tab = []
|
||||
|
||||
|
||||
def populate_global_symbol_table(tree, module: ir.Module):
|
||||
for node in tree.body:
|
||||
if isinstance(node, ast.FunctionDef):
|
||||
for dec in node.decorator_list:
|
||||
if (
|
||||
isinstance(dec, ast.Call)
|
||||
and isinstance(dec.func, ast.Name)
|
||||
and dec.func.id == "section"
|
||||
and len(dec.args) == 1
|
||||
and isinstance(dec.args[0], ast.Constant)
|
||||
and isinstance(dec.args[0].value, str)
|
||||
):
|
||||
global_sym_tab.append(node)
|
||||
elif isinstance(dec, ast.Name) and dec.id == "bpfglobal":
|
||||
global_sym_tab.append(node)
|
||||
|
||||
elif isinstance(dec, ast.Name) and dec.id == "map":
|
||||
global_sym_tab.append(node)
|
||||
return False
|
||||
|
||||
|
||||
def emit_global(module: ir.Module, node, name):
|
||||
logger.info(f"global identifier {name} processing")
|
||||
# deduce LLVM type from the annotated return
|
||||
if not isinstance(node.returns, ast.Name):
|
||||
raise ValueError(f"Unsupported return annotation {ast.dump(node.returns)}")
|
||||
ty = ctypes_to_ir(node.returns.id)
|
||||
|
||||
# extract the return expression
|
||||
# TODO: turn this return extractor into a generic function I can use everywhere.
|
||||
ret_stmt = node.body[0]
|
||||
if not isinstance(ret_stmt, ast.Return) or ret_stmt.value is None:
|
||||
raise ValueError(f"Global '{name}' has no valid return")
|
||||
|
||||
init_val = ret_stmt.value
|
||||
|
||||
# simple constant like "return 0"
|
||||
if isinstance(init_val, ast.Constant):
|
||||
llvm_init = ir.Constant(ty, init_val.value)
|
||||
|
||||
# variable reference like "return SOME_CONST"
|
||||
elif isinstance(init_val, ast.Name):
|
||||
# need symbol resolution here, stub as 0 for now
|
||||
raise ValueError(f"Name reference {init_val.id} not yet supported")
|
||||
|
||||
# constructor call like "return c_int64(0)" or dataclass(...)
|
||||
elif isinstance(init_val, ast.Call):
|
||||
if len(init_val.args) >= 1 and isinstance(init_val.args[0], ast.Constant):
|
||||
llvm_init = ir.Constant(ty, init_val.args[0].value)
|
||||
else:
|
||||
logger.info("Defaulting to zero as no constant argument found")
|
||||
llvm_init = ir.Constant(ty, 0)
|
||||
else:
|
||||
raise ValueError(f"Unsupported return expr {ast.dump(init_val)}")
|
||||
|
||||
gvar = ir.GlobalVariable(module, ty, name=name)
|
||||
gvar.initializer = llvm_init
|
||||
gvar.align = 8
|
||||
gvar.linkage = "dso_local"
|
||||
gvar.global_constant = False
|
||||
return gvar
|
||||
|
||||
|
||||
def globals_processing(tree, module):
|
||||
"""Process stuff decorated with @bpf and @bpfglobal except license and return the section name"""
|
||||
globals_sym_tab = []
|
||||
|
||||
for node in tree.body:
|
||||
# Skip non-assignment and non-function nodes
|
||||
if not (isinstance(node, ast.FunctionDef)):
|
||||
continue
|
||||
|
||||
# Get the name based on node type
|
||||
if isinstance(node, ast.FunctionDef):
|
||||
name = node.name
|
||||
else:
|
||||
continue
|
||||
|
||||
# Check for duplicate names
|
||||
if name in globals_sym_tab:
|
||||
raise SyntaxError(f"ERROR: Global name '{name}' previously defined")
|
||||
else:
|
||||
globals_sym_tab.append(name)
|
||||
|
||||
if isinstance(node, ast.FunctionDef) and node.name != "LICENSE":
|
||||
decorators = [
|
||||
dec.id for dec in node.decorator_list if isinstance(dec, ast.Name)
|
||||
]
|
||||
if "bpf" in decorators and "bpfglobal" in decorators:
|
||||
if (
|
||||
len(node.body) == 1
|
||||
and isinstance(node.body[0], ast.Return)
|
||||
and node.body[0].value is not None
|
||||
and isinstance(
|
||||
node.body[0].value, (ast.Constant, ast.Name, ast.Call)
|
||||
)
|
||||
):
|
||||
emit_global(module, node, name)
|
||||
else:
|
||||
raise SyntaxError(f"ERROR: Invalid syntax for {name} global")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def emit_llvm_compiler_used(module: ir.Module, names: list[str]):
|
||||
"""
|
||||
Emit the @llvm.compiler.used global given a list of function/global names.
|
||||
"""
|
||||
@ -24,7 +137,7 @@ def emit_globals(module: ir.Module, names: list[str]):
|
||||
gv.section = "llvm.metadata"
|
||||
|
||||
|
||||
def globals_processing(tree, module: ir.Module):
|
||||
def globals_list_creation(tree, module: ir.Module):
|
||||
collected = ["LICENSE"]
|
||||
|
||||
for node in tree.body:
|
||||
@ -40,10 +153,11 @@ def globals_processing(tree, module: ir.Module):
|
||||
):
|
||||
collected.append(node.name)
|
||||
|
||||
elif isinstance(dec, ast.Name) and dec.id == "bpfglobal":
|
||||
collected.append(node.name)
|
||||
# NOTE: all globals other than
|
||||
# elif isinstance(dec, ast.Name) and dec.id == "bpfglobal":
|
||||
# collected.append(node.name)
|
||||
|
||||
elif isinstance(dec, ast.Name) and dec.id == "map":
|
||||
collected.append(node.name)
|
||||
|
||||
emit_globals(module, collected)
|
||||
emit_llvm_compiler_used(module, collected)
|
||||
|
||||
67
pythonbpf/helper/__init__.py
Normal file
67
pythonbpf/helper/__init__.py
Normal file
@ -0,0 +1,67 @@
|
||||
from .helper_registry import HelperHandlerRegistry
|
||||
from .helper_utils import reset_scratch_pool
|
||||
from .bpf_helper_handler import handle_helper_call
|
||||
from .helpers import ktime, pid, deref, XDP_DROP, XDP_PASS
|
||||
|
||||
|
||||
# Register the helper handler with expr module
|
||||
def _register_helper_handler():
|
||||
"""Register helper call handler with the expression evaluator"""
|
||||
from pythonbpf.expr.expr_pass import CallHandlerRegistry
|
||||
|
||||
def helper_call_handler(
|
||||
call, module, builder, func, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
):
|
||||
"""Check if call is a helper and handle it"""
|
||||
import ast
|
||||
|
||||
# Check for direct helper calls (e.g., ktime(), print())
|
||||
if isinstance(call.func, ast.Name):
|
||||
if HelperHandlerRegistry.has_handler(call.func.id):
|
||||
return handle_helper_call(
|
||||
call,
|
||||
module,
|
||||
builder,
|
||||
func,
|
||||
local_sym_tab,
|
||||
map_sym_tab,
|
||||
structs_sym_tab,
|
||||
)
|
||||
|
||||
# Check for method calls (e.g., map.lookup())
|
||||
elif isinstance(call.func, ast.Attribute):
|
||||
method_name = call.func.attr
|
||||
|
||||
# Handle: my_map.lookup(key)
|
||||
if isinstance(call.func.value, ast.Name):
|
||||
obj_name = call.func.value.id
|
||||
if map_sym_tab and obj_name in map_sym_tab:
|
||||
if HelperHandlerRegistry.has_handler(method_name):
|
||||
return handle_helper_call(
|
||||
call,
|
||||
module,
|
||||
builder,
|
||||
func,
|
||||
local_sym_tab,
|
||||
map_sym_tab,
|
||||
structs_sym_tab,
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
CallHandlerRegistry.set_handler(helper_call_handler)
|
||||
|
||||
|
||||
# Register on module import
|
||||
_register_helper_handler()
|
||||
|
||||
__all__ = [
|
||||
"HelperHandlerRegistry",
|
||||
"reset_scratch_pool",
|
||||
"handle_helper_call",
|
||||
"ktime",
|
||||
"pid",
|
||||
"deref",
|
||||
"XDP_DROP",
|
||||
"XDP_PASS",
|
||||
]
|
||||
368
pythonbpf/helper/bpf_helper_handler.py
Normal file
368
pythonbpf/helper/bpf_helper_handler.py
Normal file
@ -0,0 +1,368 @@
|
||||
import ast
|
||||
from llvmlite import ir
|
||||
from enum import Enum
|
||||
|
||||
from .helper_registry import HelperHandlerRegistry
|
||||
from .helper_utils import (
|
||||
get_or_create_ptr_from_arg,
|
||||
get_flags_val,
|
||||
get_data_ptr_and_size,
|
||||
)
|
||||
from .printk_formatter import simple_string_print, handle_fstring_print
|
||||
|
||||
from logging import Logger
|
||||
import logging
|
||||
|
||||
logger: Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BPFHelperID(Enum):
|
||||
BPF_MAP_LOOKUP_ELEM = 1
|
||||
BPF_MAP_UPDATE_ELEM = 2
|
||||
BPF_MAP_DELETE_ELEM = 3
|
||||
BPF_KTIME_GET_NS = 5
|
||||
BPF_PRINTK = 6
|
||||
BPF_GET_CURRENT_PID_TGID = 14
|
||||
BPF_PERF_EVENT_OUTPUT = 25
|
||||
|
||||
|
||||
@HelperHandlerRegistry.register("ktime")
|
||||
def bpf_ktime_get_ns_emitter(
|
||||
call,
|
||||
map_ptr,
|
||||
module,
|
||||
builder,
|
||||
func,
|
||||
local_sym_tab=None,
|
||||
struct_sym_tab=None,
|
||||
map_sym_tab=None,
|
||||
):
|
||||
"""
|
||||
Emit LLVM IR for bpf_ktime_get_ns helper function call.
|
||||
"""
|
||||
# func is an arg to just have a uniform signature with other emitters
|
||||
helper_id = ir.Constant(ir.IntType(64), BPFHelperID.BPF_KTIME_GET_NS.value)
|
||||
fn_type = ir.FunctionType(ir.IntType(64), [], var_arg=False)
|
||||
fn_ptr_type = ir.PointerType(fn_type)
|
||||
fn_ptr = builder.inttoptr(helper_id, fn_ptr_type)
|
||||
result = builder.call(fn_ptr, [], tail=False)
|
||||
return result, ir.IntType(64)
|
||||
|
||||
|
||||
@HelperHandlerRegistry.register("lookup")
|
||||
def bpf_map_lookup_elem_emitter(
|
||||
call,
|
||||
map_ptr,
|
||||
module,
|
||||
builder,
|
||||
func,
|
||||
local_sym_tab=None,
|
||||
struct_sym_tab=None,
|
||||
map_sym_tab=None,
|
||||
):
|
||||
"""
|
||||
Emit LLVM IR for bpf_map_lookup_elem helper function call.
|
||||
"""
|
||||
if not call.args or len(call.args) != 1:
|
||||
raise ValueError(
|
||||
f"Map lookup expects exactly one argument (key), got {len(call.args)}"
|
||||
)
|
||||
key_ptr = get_or_create_ptr_from_arg(
|
||||
func, module, call.args[0], builder, local_sym_tab, map_sym_tab, struct_sym_tab
|
||||
)
|
||||
map_void_ptr = builder.bitcast(map_ptr, ir.PointerType())
|
||||
|
||||
# TODO: I have changed the return type to i64*, as we are
|
||||
# allocating space for that type in allocate_mem. This is
|
||||
# temporary, and we will honour other widths later. But this
|
||||
# allows us to have cool binary ops on the returned value.
|
||||
fn_type = ir.FunctionType(
|
||||
ir.PointerType(ir.IntType(64)), # Return type: void*
|
||||
[ir.PointerType(), ir.PointerType()], # Args: (void*, void*)
|
||||
var_arg=False,
|
||||
)
|
||||
fn_ptr_type = ir.PointerType(fn_type)
|
||||
|
||||
fn_addr = ir.Constant(ir.IntType(64), BPFHelperID.BPF_MAP_LOOKUP_ELEM.value)
|
||||
fn_ptr = builder.inttoptr(fn_addr, fn_ptr_type)
|
||||
|
||||
result = builder.call(fn_ptr, [map_void_ptr, key_ptr], tail=False)
|
||||
|
||||
return result, ir.PointerType()
|
||||
|
||||
|
||||
@HelperHandlerRegistry.register("print")
|
||||
def bpf_printk_emitter(
|
||||
call,
|
||||
map_ptr,
|
||||
module,
|
||||
builder,
|
||||
func,
|
||||
local_sym_tab=None,
|
||||
struct_sym_tab=None,
|
||||
map_sym_tab=None,
|
||||
):
|
||||
"""Emit LLVM IR for bpf_printk helper function call."""
|
||||
if not hasattr(func, "_fmt_counter"):
|
||||
func._fmt_counter = 0
|
||||
|
||||
if not call.args:
|
||||
raise ValueError("bpf_printk expects at least one argument (format string)")
|
||||
|
||||
args = []
|
||||
if isinstance(call.args[0], ast.JoinedStr):
|
||||
args = handle_fstring_print(
|
||||
call.args[0],
|
||||
module,
|
||||
builder,
|
||||
func,
|
||||
local_sym_tab,
|
||||
struct_sym_tab,
|
||||
)
|
||||
elif isinstance(call.args[0], ast.Constant) and isinstance(call.args[0].value, str):
|
||||
# TODO: We are only supporting single arguments for now.
|
||||
# In case of multiple args, the first one will be taken.
|
||||
args = simple_string_print(call.args[0].value, module, builder, func)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Only simple strings or f-strings are supported in bpf_printk."
|
||||
)
|
||||
|
||||
fn_type = ir.FunctionType(
|
||||
ir.IntType(64), [ir.PointerType(), ir.IntType(32)], var_arg=True
|
||||
)
|
||||
fn_ptr_type = ir.PointerType(fn_type)
|
||||
fn_addr = ir.Constant(ir.IntType(64), BPFHelperID.BPF_PRINTK.value)
|
||||
fn_ptr = builder.inttoptr(fn_addr, fn_ptr_type)
|
||||
|
||||
builder.call(fn_ptr, args, tail=True)
|
||||
return True
|
||||
|
||||
|
||||
@HelperHandlerRegistry.register("update")
|
||||
def bpf_map_update_elem_emitter(
|
||||
call,
|
||||
map_ptr,
|
||||
module,
|
||||
builder,
|
||||
func,
|
||||
local_sym_tab=None,
|
||||
struct_sym_tab=None,
|
||||
map_sym_tab=None,
|
||||
):
|
||||
"""
|
||||
Emit LLVM IR for bpf_map_update_elem helper function call.
|
||||
Expected call signature: map.update(key, value, flags=0)
|
||||
"""
|
||||
if not call.args or len(call.args) < 2 or len(call.args) > 3:
|
||||
raise ValueError(
|
||||
f"Map update expects 2 or 3 args (key, value, flags), got {len(call.args)}"
|
||||
)
|
||||
|
||||
key_arg = call.args[0]
|
||||
value_arg = call.args[1]
|
||||
flags_arg = call.args[2] if len(call.args) > 2 else None
|
||||
|
||||
key_ptr = get_or_create_ptr_from_arg(
|
||||
func, module, key_arg, builder, local_sym_tab, map_sym_tab, struct_sym_tab
|
||||
)
|
||||
value_ptr = get_or_create_ptr_from_arg(
|
||||
func, module, value_arg, builder, local_sym_tab, map_sym_tab, struct_sym_tab
|
||||
)
|
||||
flags_val = get_flags_val(flags_arg, builder, local_sym_tab)
|
||||
|
||||
map_void_ptr = builder.bitcast(map_ptr, ir.PointerType())
|
||||
fn_type = ir.FunctionType(
|
||||
ir.IntType(64),
|
||||
[ir.PointerType(), ir.PointerType(), ir.PointerType(), ir.IntType(64)],
|
||||
var_arg=False,
|
||||
)
|
||||
fn_ptr_type = ir.PointerType(fn_type)
|
||||
|
||||
fn_addr = ir.Constant(ir.IntType(64), BPFHelperID.BPF_MAP_UPDATE_ELEM.value)
|
||||
fn_ptr = builder.inttoptr(fn_addr, fn_ptr_type)
|
||||
|
||||
if isinstance(flags_val, int):
|
||||
flags_const = ir.Constant(ir.IntType(64), flags_val)
|
||||
else:
|
||||
flags_const = flags_val
|
||||
|
||||
result = builder.call(
|
||||
fn_ptr, [map_void_ptr, key_ptr, value_ptr, flags_const], tail=False
|
||||
)
|
||||
|
||||
return result, None
|
||||
|
||||
|
||||
@HelperHandlerRegistry.register("delete")
|
||||
def bpf_map_delete_elem_emitter(
|
||||
call,
|
||||
map_ptr,
|
||||
module,
|
||||
builder,
|
||||
func,
|
||||
local_sym_tab=None,
|
||||
struct_sym_tab=None,
|
||||
map_sym_tab=None,
|
||||
):
|
||||
"""
|
||||
Emit LLVM IR for bpf_map_delete_elem helper function call.
|
||||
Expected call signature: map.delete(key)
|
||||
"""
|
||||
if not call.args or len(call.args) != 1:
|
||||
raise ValueError(
|
||||
f"Map delete expects exactly one argument (key), got {len(call.args)}"
|
||||
)
|
||||
key_ptr = get_or_create_ptr_from_arg(
|
||||
func, module, call.args[0], builder, local_sym_tab, map_sym_tab, struct_sym_tab
|
||||
)
|
||||
map_void_ptr = builder.bitcast(map_ptr, ir.PointerType())
|
||||
|
||||
# Define function type for bpf_map_delete_elem
|
||||
fn_type = ir.FunctionType(
|
||||
ir.IntType(64), # Return type: int64 (status code)
|
||||
[ir.PointerType(), ir.PointerType()], # Args: (void*, void*)
|
||||
var_arg=False,
|
||||
)
|
||||
fn_ptr_type = ir.PointerType(fn_type)
|
||||
|
||||
fn_addr = ir.Constant(ir.IntType(64), BPFHelperID.BPF_MAP_DELETE_ELEM.value)
|
||||
fn_ptr = builder.inttoptr(fn_addr, fn_ptr_type)
|
||||
|
||||
result = builder.call(fn_ptr, [map_void_ptr, key_ptr], tail=False)
|
||||
|
||||
return result, None
|
||||
|
||||
|
||||
@HelperHandlerRegistry.register("pid")
|
||||
def bpf_get_current_pid_tgid_emitter(
|
||||
call,
|
||||
map_ptr,
|
||||
module,
|
||||
builder,
|
||||
func,
|
||||
local_sym_tab=None,
|
||||
struct_sym_tab=None,
|
||||
map_sym_tab=None,
|
||||
):
|
||||
"""
|
||||
Emit LLVM IR for bpf_get_current_pid_tgid helper function call.
|
||||
"""
|
||||
# func is an arg to just have a uniform signature with other emitters
|
||||
helper_id = ir.Constant(ir.IntType(64), BPFHelperID.BPF_GET_CURRENT_PID_TGID.value)
|
||||
fn_type = ir.FunctionType(ir.IntType(64), [], var_arg=False)
|
||||
fn_ptr_type = ir.PointerType(fn_type)
|
||||
fn_ptr = builder.inttoptr(helper_id, fn_ptr_type)
|
||||
result = builder.call(fn_ptr, [], tail=False)
|
||||
|
||||
# Extract the lower 32 bits (PID) using bitwise AND with 0xFFFFFFFF
|
||||
mask = ir.Constant(ir.IntType(64), 0xFFFFFFFF)
|
||||
pid = builder.and_(result, mask)
|
||||
return pid, ir.IntType(64)
|
||||
|
||||
|
||||
@HelperHandlerRegistry.register("output")
|
||||
def bpf_perf_event_output_handler(
|
||||
call,
|
||||
map_ptr,
|
||||
module,
|
||||
builder,
|
||||
func,
|
||||
local_sym_tab=None,
|
||||
struct_sym_tab=None,
|
||||
map_sym_tab=None,
|
||||
):
|
||||
if len(call.args) != 1:
|
||||
raise ValueError(
|
||||
f"Perf event output expects exactly one argument, got {len(call.args)}"
|
||||
)
|
||||
data_arg = call.args[0]
|
||||
ctx_ptr = func.args[0] # First argument to the function is ctx
|
||||
|
||||
data_ptr, size_val = get_data_ptr_and_size(data_arg, local_sym_tab, struct_sym_tab)
|
||||
|
||||
# BPF_F_CURRENT_CPU is -1 in 32 bit
|
||||
flags_val = ir.Constant(ir.IntType(64), 0xFFFFFFFF)
|
||||
|
||||
map_void_ptr = builder.bitcast(map_ptr, ir.PointerType())
|
||||
data_void_ptr = builder.bitcast(data_ptr, ir.PointerType())
|
||||
fn_type = ir.FunctionType(
|
||||
ir.IntType(64),
|
||||
[
|
||||
ir.PointerType(ir.IntType(8)),
|
||||
ir.PointerType(),
|
||||
ir.IntType(64),
|
||||
ir.PointerType(),
|
||||
ir.IntType(64),
|
||||
],
|
||||
var_arg=False,
|
||||
)
|
||||
fn_ptr_type = ir.PointerType(fn_type)
|
||||
|
||||
# helper id
|
||||
fn_addr = ir.Constant(ir.IntType(64), BPFHelperID.BPF_PERF_EVENT_OUTPUT.value)
|
||||
fn_ptr = builder.inttoptr(fn_addr, fn_ptr_type)
|
||||
|
||||
result = builder.call(
|
||||
fn_ptr, [ctx_ptr, map_void_ptr, flags_val, data_void_ptr, size_val], tail=False
|
||||
)
|
||||
return result, None
|
||||
|
||||
|
||||
def handle_helper_call(
|
||||
call,
|
||||
module,
|
||||
builder,
|
||||
func,
|
||||
local_sym_tab=None,
|
||||
map_sym_tab=None,
|
||||
struct_sym_tab=None,
|
||||
):
|
||||
"""Process a BPF helper function call and emit the appropriate LLVM IR."""
|
||||
|
||||
# Helper function to get map pointer and invoke handler
|
||||
def invoke_helper(method_name, map_ptr=None):
|
||||
handler = HelperHandlerRegistry.get_handler(method_name)
|
||||
if not handler:
|
||||
raise NotImplementedError(
|
||||
f"Helper function '{method_name}' is not implemented."
|
||||
)
|
||||
return handler(
|
||||
call,
|
||||
map_ptr,
|
||||
module,
|
||||
builder,
|
||||
func,
|
||||
local_sym_tab,
|
||||
struct_sym_tab,
|
||||
map_sym_tab,
|
||||
)
|
||||
|
||||
# Handle direct function calls (e.g., print(), ktime())
|
||||
if isinstance(call.func, ast.Name):
|
||||
return invoke_helper(call.func.id)
|
||||
|
||||
# Handle method calls (e.g., map.lookup(), map.update())
|
||||
elif isinstance(call.func, ast.Attribute):
|
||||
method_name = call.func.attr
|
||||
value = call.func.value
|
||||
logger.info(f"Handling method call: {ast.dump(call.func)}")
|
||||
# Get map pointer from different styles of map access
|
||||
if isinstance(value, ast.Call) and isinstance(value.func, ast.Name):
|
||||
# Func style: my_map().lookup(key)
|
||||
map_name = value.func.id
|
||||
elif isinstance(value, ast.Name):
|
||||
# Direct style: my_map.lookup(key)
|
||||
map_name = value.id
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Unsupported map access pattern: {ast.dump(value)}"
|
||||
)
|
||||
|
||||
# Verify map exists and get pointer
|
||||
if not map_sym_tab or map_name not in map_sym_tab:
|
||||
raise ValueError(f"Map '{map_name}' not found in symbol table")
|
||||
|
||||
return invoke_helper(method_name, map_sym_tab[map_name])
|
||||
|
||||
return None
|
||||
27
pythonbpf/helper/helper_registry.py
Normal file
27
pythonbpf/helper/helper_registry.py
Normal file
@ -0,0 +1,27 @@
|
||||
from typing import Callable
|
||||
|
||||
|
||||
class HelperHandlerRegistry:
|
||||
"""Registry for BPF helpers"""
|
||||
|
||||
_handlers: dict[str, Callable] = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, helper_name):
|
||||
"""Decorator to register a handler function for a helper"""
|
||||
|
||||
def decorator(func):
|
||||
cls._handlers[helper_name] = func
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
@classmethod
|
||||
def get_handler(cls, helper_name):
|
||||
"""Get the handler function for a helper"""
|
||||
return cls._handlers.get(helper_name)
|
||||
|
||||
@classmethod
|
||||
def has_handler(cls, helper_name):
|
||||
"""Check if a handler function is registered for a helper"""
|
||||
return helper_name in cls._handlers
|
||||
138
pythonbpf/helper/helper_utils.py
Normal file
138
pythonbpf/helper/helper_utils.py
Normal file
@ -0,0 +1,138 @@
|
||||
import ast
|
||||
import logging
|
||||
|
||||
from llvmlite import ir
|
||||
from pythonbpf.expr import (
|
||||
get_operand_value,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ScratchPoolManager:
|
||||
"""Manage the temporary helper variables in local_sym_tab"""
|
||||
|
||||
def __init__(self):
|
||||
self._counter = 0
|
||||
|
||||
@property
|
||||
def counter(self):
|
||||
return self._counter
|
||||
|
||||
def reset(self):
|
||||
self._counter = 0
|
||||
logger.debug("Scratch pool counter reset to 0")
|
||||
|
||||
def get_next_temp(self, local_sym_tab):
|
||||
temp_name = f"__helper_temp_{self._counter}"
|
||||
self._counter += 1
|
||||
|
||||
if temp_name not in local_sym_tab:
|
||||
raise ValueError(
|
||||
f"Scratch pool exhausted or inadequate: {temp_name}. "
|
||||
f"Current counter: {self._counter}"
|
||||
)
|
||||
|
||||
return local_sym_tab[temp_name].var, temp_name
|
||||
|
||||
|
||||
_temp_pool_manager = ScratchPoolManager() # Singleton instance
|
||||
|
||||
|
||||
def reset_scratch_pool():
|
||||
"""Reset the scratch pool counter"""
|
||||
_temp_pool_manager.reset()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Argument Preparation
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_var_ptr_from_name(var_name, local_sym_tab):
|
||||
"""Get a pointer to a variable from the symbol table."""
|
||||
if local_sym_tab and var_name in local_sym_tab:
|
||||
return local_sym_tab[var_name].var
|
||||
raise ValueError(f"Variable '{var_name}' not found in local symbol table")
|
||||
|
||||
|
||||
def create_int_constant_ptr(value, builder, local_sym_tab, int_width=64):
|
||||
"""Create a pointer to an integer constant."""
|
||||
|
||||
# Default to 64-bit integer
|
||||
ptr, temp_name = _temp_pool_manager.get_next_temp(local_sym_tab)
|
||||
logger.info(f"Using temp variable '{temp_name}' for int constant {value}")
|
||||
const_val = ir.Constant(ir.IntType(int_width), value)
|
||||
builder.store(const_val, ptr)
|
||||
return ptr
|
||||
|
||||
|
||||
def get_or_create_ptr_from_arg(
|
||||
func, module, arg, builder, local_sym_tab, map_sym_tab, struct_sym_tab=None
|
||||
):
|
||||
"""Extract or create pointer from the call arguments."""
|
||||
|
||||
if isinstance(arg, ast.Name):
|
||||
ptr = get_var_ptr_from_name(arg.id, local_sym_tab)
|
||||
elif isinstance(arg, ast.Constant) and isinstance(arg.value, int):
|
||||
ptr = create_int_constant_ptr(arg.value, builder, local_sym_tab)
|
||||
else:
|
||||
# Evaluate the expression and store the result in a temp variable
|
||||
val = get_operand_value(
|
||||
func, module, arg, builder, local_sym_tab, map_sym_tab, struct_sym_tab
|
||||
)
|
||||
if val is None:
|
||||
raise ValueError("Failed to evaluate expression for helper arg.")
|
||||
|
||||
# NOTE: We assume the result is an int64 for now
|
||||
# if isinstance(arg, ast.Attribute):
|
||||
# return val
|
||||
ptr, temp_name = _temp_pool_manager.get_next_temp(local_sym_tab)
|
||||
logger.info(f"Using temp variable '{temp_name}' for expression result")
|
||||
builder.store(val, ptr)
|
||||
|
||||
return ptr
|
||||
|
||||
|
||||
def get_flags_val(arg, builder, local_sym_tab):
|
||||
"""Extract or create flags value from the call arguments."""
|
||||
if not arg:
|
||||
return 0
|
||||
|
||||
if isinstance(arg, ast.Name):
|
||||
if local_sym_tab and arg.id in local_sym_tab:
|
||||
flags_ptr = local_sym_tab[arg.id].var
|
||||
return builder.load(flags_ptr)
|
||||
else:
|
||||
raise ValueError(f"Variable '{arg.id}' not found in local symbol table")
|
||||
elif isinstance(arg, ast.Constant) and isinstance(arg.value, int):
|
||||
return arg.value
|
||||
|
||||
raise NotImplementedError(
|
||||
"Only var names or int consts are supported as map helpers flags."
|
||||
)
|
||||
|
||||
|
||||
def get_data_ptr_and_size(data_arg, local_sym_tab, struct_sym_tab):
|
||||
"""Extract data pointer and size information for perf event output."""
|
||||
if isinstance(data_arg, ast.Name):
|
||||
data_name = data_arg.id
|
||||
if local_sym_tab and data_name in local_sym_tab:
|
||||
data_ptr = local_sym_tab[data_name].var
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Data variable {data_name} not found in local symbol table."
|
||||
)
|
||||
|
||||
# Check if data_name is a struct
|
||||
data_type = local_sym_tab[data_name].metadata
|
||||
if data_type in struct_sym_tab:
|
||||
struct_info = struct_sym_tab[data_type]
|
||||
size_val = ir.Constant(ir.IntType(64), struct_info.size)
|
||||
return data_ptr, size_val
|
||||
else:
|
||||
raise ValueError(f"Struct {data_type} for {data_name} not in symbol table.")
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Only simple object names are supported as data in perf event output."
|
||||
)
|
||||
@ -1,15 +1,22 @@
|
||||
import ctypes
|
||||
|
||||
|
||||
def ktime():
|
||||
return ctypes.c_int64(0)
|
||||
|
||||
|
||||
def pid():
|
||||
return ctypes.c_int32(0)
|
||||
|
||||
|
||||
def deref(ptr):
|
||||
"dereference a pointer"
|
||||
result = ctypes.cast(ptr, ctypes.POINTER(ctypes.c_void_p)).contents.value
|
||||
return result if result is not None else 0
|
||||
|
||||
|
||||
XDP_ABORTED = ctypes.c_int64(0)
|
||||
XDP_DROP = ctypes.c_int64(1)
|
||||
XDP_PASS = ctypes.c_int64(2)
|
||||
XDP_TX = ctypes.c_int64(3)
|
||||
XDP_REDIRECT = ctypes.c_int64(4)
|
||||
240
pythonbpf/helper/printk_formatter.py
Normal file
240
pythonbpf/helper/printk_formatter.py
Normal file
@ -0,0 +1,240 @@
|
||||
import ast
|
||||
import logging
|
||||
|
||||
from llvmlite import ir
|
||||
from pythonbpf.expr import eval_expr, get_base_type_and_depth, deref_to_depth
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def simple_string_print(string_value, module, builder, func):
|
||||
"""Prepare arguments for bpf_printk from a simple string value"""
|
||||
fmt_str = string_value + "\n\0"
|
||||
fmt_ptr = _create_format_string_global(fmt_str, func, module, builder)
|
||||
|
||||
args = [fmt_ptr, ir.Constant(ir.IntType(32), len(fmt_str))]
|
||||
return args
|
||||
|
||||
|
||||
def handle_fstring_print(
|
||||
joined_str,
|
||||
module,
|
||||
builder,
|
||||
func,
|
||||
local_sym_tab=None,
|
||||
struct_sym_tab=None,
|
||||
):
|
||||
"""Handle f-string formatting for bpf_printk emitter."""
|
||||
fmt_parts = []
|
||||
exprs = []
|
||||
|
||||
for value in joined_str.values:
|
||||
logger.debug(f"Processing f-string value: {ast.dump(value)}")
|
||||
|
||||
if isinstance(value, ast.Constant):
|
||||
_process_constant_in_fstring(value, fmt_parts, exprs)
|
||||
elif isinstance(value, ast.FormattedValue):
|
||||
_process_fval(
|
||||
value,
|
||||
fmt_parts,
|
||||
exprs,
|
||||
local_sym_tab,
|
||||
struct_sym_tab,
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(f"Unsupported f-string value type: {type(value)}")
|
||||
|
||||
fmt_str = "".join(fmt_parts)
|
||||
args = simple_string_print(fmt_str, module, builder, func)
|
||||
|
||||
# NOTE: Process expressions (limited to 3 due to BPF constraints)
|
||||
if len(exprs) > 3:
|
||||
logger.warning("bpf_printk supports up to 3 args, extra args will be ignored.")
|
||||
|
||||
for expr in exprs[:3]:
|
||||
arg_value = _prepare_expr_args(
|
||||
expr,
|
||||
func,
|
||||
module,
|
||||
builder,
|
||||
local_sym_tab,
|
||||
struct_sym_tab,
|
||||
)
|
||||
args.append(arg_value)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Internal Helpers
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def _process_constant_in_fstring(cst, fmt_parts, exprs):
|
||||
"""Process constant values in f-string."""
|
||||
if isinstance(cst.value, str):
|
||||
fmt_parts.append(cst.value)
|
||||
elif isinstance(cst.value, int):
|
||||
fmt_parts.append("%lld")
|
||||
exprs.append(ir.Constant(ir.IntType(64), cst.value))
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Unsupported constant type in f-string: {type(cst.value)}"
|
||||
)
|
||||
|
||||
|
||||
def _process_fval(fval, fmt_parts, exprs, local_sym_tab, struct_sym_tab):
|
||||
"""Process formatted values in f-string."""
|
||||
logger.debug(f"Processing formatted value: {ast.dump(fval)}")
|
||||
|
||||
if isinstance(fval.value, ast.Name):
|
||||
_process_name_in_fval(fval.value, fmt_parts, exprs, local_sym_tab)
|
||||
elif isinstance(fval.value, ast.Attribute):
|
||||
_process_attr_in_fval(
|
||||
fval.value,
|
||||
fmt_parts,
|
||||
exprs,
|
||||
local_sym_tab,
|
||||
struct_sym_tab,
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Unsupported formatted value in f-string: {type(fval.value)}"
|
||||
)
|
||||
|
||||
|
||||
def _process_name_in_fval(name_node, fmt_parts, exprs, local_sym_tab):
|
||||
"""Process name nodes in formatted values."""
|
||||
if local_sym_tab and name_node.id in local_sym_tab:
|
||||
_, var_type, tmp = local_sym_tab[name_node.id]
|
||||
_populate_fval(var_type, name_node, fmt_parts, exprs)
|
||||
|
||||
|
||||
def _process_attr_in_fval(attr_node, fmt_parts, exprs, local_sym_tab, struct_sym_tab):
|
||||
"""Process attribute nodes in formatted values."""
|
||||
if (
|
||||
isinstance(attr_node.value, ast.Name)
|
||||
and local_sym_tab
|
||||
and attr_node.value.id in local_sym_tab
|
||||
):
|
||||
var_name = attr_node.value.id
|
||||
field_name = attr_node.attr
|
||||
|
||||
var_type = local_sym_tab[var_name].metadata
|
||||
if var_type not in struct_sym_tab:
|
||||
raise ValueError(
|
||||
f"Struct '{var_type}' for '{var_name}' not in symbol table"
|
||||
)
|
||||
|
||||
struct_info = struct_sym_tab[var_type]
|
||||
if field_name not in struct_info.fields:
|
||||
raise ValueError(f"Field '{field_name}' not found in struct '{var_type}'")
|
||||
|
||||
field_type = struct_info.field_type(field_name)
|
||||
_populate_fval(field_type, attr_node, fmt_parts, exprs)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Only simple attribute on local vars is supported in f-strings."
|
||||
)
|
||||
|
||||
|
||||
def _populate_fval(ftype, node, fmt_parts, exprs):
|
||||
"""Populate format parts and expressions based on field type."""
|
||||
if isinstance(ftype, ir.IntType):
|
||||
# TODO: We print as signed integers only for now
|
||||
if ftype.width == 64:
|
||||
fmt_parts.append("%lld")
|
||||
exprs.append(node)
|
||||
elif ftype.width == 32:
|
||||
fmt_parts.append("%d")
|
||||
exprs.append(node)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Unsupported integer width in f-string: {ftype.width}"
|
||||
)
|
||||
elif isinstance(ftype, ir.PointerType):
|
||||
target, depth = get_base_type_and_depth(ftype)
|
||||
if isinstance(target, ir.IntType):
|
||||
if target.width == 64:
|
||||
fmt_parts.append("%lld")
|
||||
exprs.append(node)
|
||||
elif target.width == 32:
|
||||
fmt_parts.append("%d")
|
||||
exprs.append(node)
|
||||
elif target.width == 8 and depth == 1:
|
||||
# NOTE: Assume i8* is a string
|
||||
fmt_parts.append("%s")
|
||||
exprs.append(node)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Unsupported pointer target type in f-string: {target}"
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Unsupported pointer target type in f-string: {target}"
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(f"Unsupported field type in f-string: {ftype}")
|
||||
|
||||
|
||||
def _create_format_string_global(fmt_str, func, module, builder):
|
||||
"""Create a global variable for the format string."""
|
||||
fmt_name = f"{func.name}____fmt{func._fmt_counter}"
|
||||
func._fmt_counter += 1
|
||||
|
||||
fmt_gvar = ir.GlobalVariable(
|
||||
module, ir.ArrayType(ir.IntType(8), len(fmt_str)), name=fmt_name
|
||||
)
|
||||
fmt_gvar.global_constant = True
|
||||
fmt_gvar.initializer = ir.Constant(
|
||||
ir.ArrayType(ir.IntType(8), len(fmt_str)), bytearray(fmt_str.encode("utf8"))
|
||||
)
|
||||
fmt_gvar.linkage = "internal"
|
||||
fmt_gvar.align = 1
|
||||
|
||||
return builder.bitcast(fmt_gvar, ir.PointerType())
|
||||
|
||||
|
||||
def _prepare_expr_args(expr, func, module, builder, local_sym_tab, struct_sym_tab):
|
||||
"""Evaluate and prepare an expression to use as an arg for bpf_printk."""
|
||||
val, _ = eval_expr(
|
||||
func,
|
||||
module,
|
||||
builder,
|
||||
expr,
|
||||
local_sym_tab,
|
||||
None,
|
||||
struct_sym_tab,
|
||||
)
|
||||
|
||||
if val:
|
||||
if isinstance(val.type, ir.PointerType):
|
||||
target, depth = get_base_type_and_depth(val.type)
|
||||
if isinstance(target, ir.IntType):
|
||||
if target.width >= 32:
|
||||
val = deref_to_depth(func, builder, val, depth)
|
||||
val = builder.sext(val, ir.IntType(64))
|
||||
elif target.width == 8 and depth == 1:
|
||||
# NOTE: i8* is string, no need to deref
|
||||
pass
|
||||
|
||||
else:
|
||||
logger.warning(
|
||||
"Only int and ptr supported in bpf_printk args. Others default to 0."
|
||||
)
|
||||
val = ir.Constant(ir.IntType(64), 0)
|
||||
elif isinstance(val.type, ir.IntType):
|
||||
if val.type.width < 64:
|
||||
val = builder.sext(val, ir.IntType(64))
|
||||
else:
|
||||
logger.warning(
|
||||
"Only int and ptr supported in bpf_printk args. Others default to 0."
|
||||
)
|
||||
val = ir.Constant(ir.IntType(64), 0)
|
||||
return val
|
||||
else:
|
||||
logger.warning(
|
||||
"Failed to evaluate expression for bpf_printk argument. "
|
||||
"It will be converted to 0."
|
||||
)
|
||||
return ir.Constant(ir.IntType(64), 0)
|
||||
@ -1,5 +1,9 @@
|
||||
from llvmlite import ir
|
||||
import ast
|
||||
from logging import Logger
|
||||
import logging
|
||||
|
||||
logger: Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def emit_license(module: ir.Module, license_str: str):
|
||||
@ -11,10 +15,10 @@ def emit_license(module: ir.Module, license_str: str):
|
||||
|
||||
gvar.initializer = ir.Constant(ty, elems) # type: ignore
|
||||
|
||||
gvar.align = 1 # type: ignore
|
||||
gvar.linkage = "dso_local" # type: ignore
|
||||
gvar.align = 1 # type: ignore
|
||||
gvar.linkage = "dso_local" # type: ignore
|
||||
gvar.global_constant = False
|
||||
gvar.section = "license" # type: ignore
|
||||
gvar.section = "license" # type: ignore
|
||||
|
||||
return gvar
|
||||
|
||||
@ -26,7 +30,8 @@ def license_processing(tree, module):
|
||||
if isinstance(node, ast.FunctionDef) and node.name == "LICENSE":
|
||||
# check decorators
|
||||
decorators = [
|
||||
dec.id for dec in node.decorator_list if isinstance(dec, ast.Name)]
|
||||
dec.id for dec in node.decorator_list if isinstance(dec, ast.Name)
|
||||
]
|
||||
if "bpf" in decorators and "bpfglobal" in decorators:
|
||||
if count == 0:
|
||||
count += 1
|
||||
@ -40,9 +45,9 @@ def license_processing(tree, module):
|
||||
emit_license(module, node.body[0].value.value)
|
||||
return "LICENSE"
|
||||
else:
|
||||
print("ERROR: LICENSE() must return a string literal")
|
||||
logger.info("ERROR: LICENSE() must return a string literal")
|
||||
return None
|
||||
else:
|
||||
print("ERROR: LICENSE already defined")
|
||||
logger.info("ERROR: LICENSE already defined")
|
||||
return None
|
||||
return None
|
||||
|
||||
4
pythonbpf/maps/__init__.py
Normal file
4
pythonbpf/maps/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from .maps import HashMap, PerfEventArray, RingBuf
|
||||
from .maps_pass import maps_proc
|
||||
|
||||
__all__ = ["HashMap", "PerfEventArray", "maps_proc", "RingBuf"]
|
||||
93
pythonbpf/maps/map_debug_info.py
Normal file
93
pythonbpf/maps/map_debug_info.py
Normal file
@ -0,0 +1,93 @@
|
||||
from pythonbpf.debuginfo import DebugInfoGenerator
|
||||
from .map_types import BPFMapType
|
||||
|
||||
|
||||
def create_map_debug_info(module, map_global, map_name, map_params):
|
||||
"""Generate debug info metadata for BPF maps HASH and PERF_EVENT_ARRAY"""
|
||||
generator = DebugInfoGenerator(module)
|
||||
|
||||
uint_type = generator.get_uint32_type()
|
||||
ulong_type = generator.get_uint64_type()
|
||||
array_type = generator.create_array_type(
|
||||
uint_type, map_params.get("type", BPFMapType.UNSPEC).value
|
||||
)
|
||||
type_ptr = generator.create_pointer_type(array_type, 64)
|
||||
key_ptr = generator.create_pointer_type(
|
||||
array_type if "key_size" in map_params else ulong_type, 64
|
||||
)
|
||||
value_ptr = generator.create_pointer_type(
|
||||
array_type if "value_size" in map_params else ulong_type, 64
|
||||
)
|
||||
|
||||
elements_arr = []
|
||||
|
||||
# Create struct members
|
||||
# scope field does not appear for some reason
|
||||
cnt = 0
|
||||
for elem in map_params:
|
||||
if elem == "max_entries":
|
||||
continue
|
||||
if elem == "type":
|
||||
ptr = type_ptr
|
||||
elif "key" in elem:
|
||||
ptr = key_ptr
|
||||
else:
|
||||
ptr = value_ptr
|
||||
# TODO: the best way to do this is not 64, but get the size each time. this will not work for structs.
|
||||
member = generator.create_struct_member(elem, ptr, cnt * 64)
|
||||
elements_arr.append(member)
|
||||
cnt += 1
|
||||
|
||||
if "max_entries" in map_params:
|
||||
max_entries_array = generator.create_array_type(
|
||||
uint_type, map_params["max_entries"]
|
||||
)
|
||||
max_entries_ptr = generator.create_pointer_type(max_entries_array, 64)
|
||||
max_entries_member = generator.create_struct_member(
|
||||
"max_entries", max_entries_ptr, cnt * 64
|
||||
)
|
||||
elements_arr.append(max_entries_member)
|
||||
|
||||
# Create the struct type
|
||||
struct_type = generator.create_struct_type(
|
||||
elements_arr, 64 * len(elements_arr), is_distinct=True
|
||||
)
|
||||
|
||||
# Create global variable debug info
|
||||
global_var = generator.create_global_var_debug_info(
|
||||
map_name, struct_type, is_local=False
|
||||
)
|
||||
|
||||
# Attach debug info to the global variable
|
||||
map_global.set_metadata("dbg", global_var)
|
||||
|
||||
return global_var
|
||||
|
||||
|
||||
def create_ringbuf_debug_info(module, map_global, map_name, map_params):
|
||||
"""Generate debug information metadata for BPF RINGBUF map"""
|
||||
generator = DebugInfoGenerator(module)
|
||||
|
||||
int_type = generator.get_int32_type()
|
||||
|
||||
type_array = generator.create_array_type(
|
||||
int_type, map_params.get("type", BPFMapType.RINGBUF).value
|
||||
)
|
||||
type_ptr = generator.create_pointer_type(type_array, 64)
|
||||
type_member = generator.create_struct_member("type", type_ptr, 0)
|
||||
|
||||
max_entries_array = generator.create_array_type(int_type, map_params["max_entries"])
|
||||
max_entries_ptr = generator.create_pointer_type(max_entries_array, 64)
|
||||
max_entries_member = generator.create_struct_member(
|
||||
"max_entries", max_entries_ptr, 64
|
||||
)
|
||||
|
||||
elements_arr = [type_member, max_entries_member]
|
||||
|
||||
struct_type = generator.create_struct_type(elements_arr, 128, is_distinct=True)
|
||||
|
||||
global_var = generator.create_global_var_debug_info(
|
||||
map_name, struct_type, is_local=False
|
||||
)
|
||||
map_global.set_metadata("dbg", global_var)
|
||||
return global_var
|
||||
39
pythonbpf/maps/map_types.py
Normal file
39
pythonbpf/maps/map_types.py
Normal file
@ -0,0 +1,39 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class BPFMapType(Enum):
|
||||
UNSPEC = 0
|
||||
HASH = 1
|
||||
ARRAY = 2
|
||||
PROG_ARRAY = 3
|
||||
PERF_EVENT_ARRAY = 4
|
||||
PERCPU_HASH = 5
|
||||
PERCPU_ARRAY = 6
|
||||
STACK_TRACE = 7
|
||||
CGROUP_ARRAY = 8
|
||||
LRU_HASH = 9
|
||||
LRU_PERCPU_HASH = 10
|
||||
LPM_TRIE = 11
|
||||
ARRAY_OF_MAPS = 12
|
||||
HASH_OF_MAPS = 13
|
||||
DEVMAP = 14
|
||||
SOCKMAP = 15
|
||||
CPUMAP = 16
|
||||
XSKMAP = 17
|
||||
SOCKHASH = 18
|
||||
CGROUP_STORAGE_DEPRECATED = 19
|
||||
CGROUP_STORAGE = 19
|
||||
REUSEPORT_SOCKARRAY = 20
|
||||
PERCPU_CGROUP_STORAGE_DEPRECATED = 21
|
||||
PERCPU_CGROUP_STORAGE = 21
|
||||
QUEUE = 22
|
||||
STACK = 23
|
||||
SK_STORAGE = 24
|
||||
DEVMAP_HASH = 25
|
||||
STRUCT_OPS = 26
|
||||
RINGBUF = 27
|
||||
INODE_STORAGE = 28
|
||||
TASK_STORAGE = 29
|
||||
BLOOM_FILTER = 30
|
||||
USER_RINGBUF = 31
|
||||
CGRP_STORAGE = 32
|
||||
@ -1,3 +1,4 @@
|
||||
# This file provides type and function hints only and does not actually give any functionality.
|
||||
class HashMap:
|
||||
def __init__(self, key, value, max_entries):
|
||||
self.key = key
|
||||
@ -33,3 +34,18 @@ class PerfEventArray:
|
||||
|
||||
def output(self, data):
|
||||
pass # Placeholder for output method
|
||||
|
||||
|
||||
class RingBuf:
|
||||
def __init__(self, max_entries):
|
||||
self.max_entries = max_entries
|
||||
|
||||
def reserve(self, size: int, flags=0):
|
||||
if size > self.max_entries:
|
||||
raise ValueError("size cannot be greater than set maximum entries")
|
||||
return 0
|
||||
|
||||
def submit(self, data, flags=0):
|
||||
pass
|
||||
|
||||
# add discard, output and also give names to flags and stuff
|
||||
141
pythonbpf/maps/maps_pass.py
Normal file
141
pythonbpf/maps/maps_pass.py
Normal file
@ -0,0 +1,141 @@
|
||||
import ast
|
||||
import logging
|
||||
from logging import Logger
|
||||
from llvmlite import ir
|
||||
|
||||
from .maps_utils import MapProcessorRegistry
|
||||
from .map_types import BPFMapType
|
||||
from .map_debug_info import create_map_debug_info, create_ringbuf_debug_info
|
||||
|
||||
logger: Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def maps_proc(tree, module, chunks):
|
||||
"""Process all functions decorated with @map to find BPF maps"""
|
||||
map_sym_tab = {}
|
||||
for func_node in chunks:
|
||||
if is_map(func_node):
|
||||
logger.info(f"Found BPF map: {func_node.name}")
|
||||
map_sym_tab[func_node.name] = process_bpf_map(func_node, module)
|
||||
return map_sym_tab
|
||||
|
||||
|
||||
def is_map(func_node):
|
||||
return any(
|
||||
isinstance(decorator, ast.Name) and decorator.id == "map"
|
||||
for decorator in func_node.decorator_list
|
||||
)
|
||||
|
||||
|
||||
def create_bpf_map(module, map_name, map_params):
|
||||
"""Create a BPF map in the module with given parameters and debug info"""
|
||||
|
||||
# Create the anonymous struct type for BPF map
|
||||
map_struct_type = ir.LiteralStructType(
|
||||
[ir.PointerType() for _ in range(len(map_params))]
|
||||
)
|
||||
|
||||
# Create the global variable
|
||||
map_global = ir.GlobalVariable(module, map_struct_type, name=map_name)
|
||||
map_global.linkage = "dso_local"
|
||||
map_global.global_constant = False
|
||||
map_global.initializer = ir.Constant(map_struct_type, None)
|
||||
map_global.section = ".maps"
|
||||
map_global.align = 8
|
||||
|
||||
logger.info(f"Created BPF map: {map_name} with params {map_params}")
|
||||
return map_global
|
||||
|
||||
|
||||
def _parse_map_params(rval, expected_args=None):
|
||||
"""Parse map parameters from call arguments and keywords."""
|
||||
|
||||
params = {}
|
||||
|
||||
# Parse positional arguments
|
||||
if expected_args:
|
||||
for i, arg_name in enumerate(expected_args):
|
||||
if i < len(rval.args):
|
||||
arg = rval.args[i]
|
||||
if isinstance(arg, ast.Name):
|
||||
params[arg_name] = arg.id
|
||||
elif isinstance(arg, ast.Constant):
|
||||
params[arg_name] = arg.value
|
||||
|
||||
# Parse keyword arguments (override positional)
|
||||
for keyword in rval.keywords:
|
||||
if isinstance(keyword.value, ast.Name):
|
||||
params[keyword.arg] = keyword.value.id
|
||||
elif isinstance(keyword.value, ast.Constant):
|
||||
params[keyword.arg] = keyword.value.value
|
||||
|
||||
return params
|
||||
|
||||
|
||||
@MapProcessorRegistry.register("RingBuf")
|
||||
def process_ringbuf_map(map_name, rval, module):
|
||||
"""Process a BPF_RINGBUF map declaration"""
|
||||
logger.info(f"Processing Ringbuf: {map_name}")
|
||||
map_params = _parse_map_params(rval, expected_args=["max_entries"])
|
||||
map_params["type"] = BPFMapType.RINGBUF
|
||||
|
||||
logger.info(f"Ringbuf map parameters: {map_params}")
|
||||
|
||||
map_global = create_bpf_map(module, map_name, map_params)
|
||||
create_ringbuf_debug_info(module, map_global, map_name, map_params)
|
||||
return map_global
|
||||
|
||||
|
||||
@MapProcessorRegistry.register("HashMap")
|
||||
def process_hash_map(map_name, rval, module):
|
||||
"""Process a BPF_HASH map declaration"""
|
||||
logger.info(f"Processing HashMap: {map_name}")
|
||||
map_params = _parse_map_params(rval, expected_args=["key", "value", "max_entries"])
|
||||
map_params["type"] = BPFMapType.HASH
|
||||
|
||||
logger.info(f"Map parameters: {map_params}")
|
||||
map_global = create_bpf_map(module, map_name, map_params)
|
||||
# Generate debug info for BTF
|
||||
create_map_debug_info(module, map_global, map_name, map_params)
|
||||
return map_global
|
||||
|
||||
|
||||
@MapProcessorRegistry.register("PerfEventArray")
|
||||
def process_perf_event_map(map_name, rval, module):
|
||||
"""Process a BPF_PERF_EVENT_ARRAY map declaration"""
|
||||
logger.info(f"Processing PerfEventArray: {map_name}")
|
||||
map_params = _parse_map_params(rval, expected_args=["key_size", "value_size"])
|
||||
map_params["type"] = BPFMapType.PERF_EVENT_ARRAY
|
||||
|
||||
logger.info(f"Map parameters: {map_params}")
|
||||
map_global = create_bpf_map(module, map_name, map_params)
|
||||
# Generate debug info for BTF
|
||||
create_map_debug_info(module, map_global, map_name, map_params)
|
||||
return map_global
|
||||
|
||||
|
||||
def process_bpf_map(func_node, module):
|
||||
"""Process a BPF map (a function decorated with @map)"""
|
||||
map_name = func_node.name
|
||||
logger.info(f"Processing BPF map: {map_name}")
|
||||
|
||||
# For now, assume single return statement
|
||||
return_stmt = None
|
||||
for stmt in func_node.body:
|
||||
if isinstance(stmt, ast.Return):
|
||||
return_stmt = stmt
|
||||
break
|
||||
if return_stmt is None:
|
||||
raise ValueError("BPF map must have a return statement")
|
||||
|
||||
rval = return_stmt.value
|
||||
|
||||
if isinstance(rval, ast.Call) and isinstance(rval.func, ast.Name):
|
||||
handler = MapProcessorRegistry.get_processor(rval.func.id)
|
||||
if handler:
|
||||
return handler(map_name, rval, module)
|
||||
else:
|
||||
logger.warning(f"Unknown map type {rval.func.id}, defaulting to HashMap")
|
||||
return process_hash_map(map_name, rval, module)
|
||||
else:
|
||||
raise ValueError("Function under @map must return a map")
|
||||
23
pythonbpf/maps/maps_utils.py
Normal file
23
pythonbpf/maps/maps_utils.py
Normal file
@ -0,0 +1,23 @@
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
|
||||
class MapProcessorRegistry:
|
||||
"""Registry for map processor functions"""
|
||||
|
||||
_processors: dict[str, Callable[..., Any]] = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, map_type_name):
|
||||
"""Decorator to register a processor function for a map type"""
|
||||
|
||||
def decorator(func):
|
||||
cls._processors[map_type_name] = func
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
@classmethod
|
||||
def get_processor(cls, map_type_name):
|
||||
"""Get the processor function for a map type"""
|
||||
return cls._processors.get(map_type_name)
|
||||
@ -1,262 +0,0 @@
|
||||
import ast
|
||||
from llvmlite import ir
|
||||
from .type_deducer import ctypes_to_ir
|
||||
from . import dwarf_constants as dc
|
||||
|
||||
map_sym_tab = {}
|
||||
|
||||
|
||||
def maps_proc(tree, module, chunks):
|
||||
for func_node in chunks:
|
||||
# Check if this function is a map
|
||||
is_map = False
|
||||
for decorator in func_node.decorator_list:
|
||||
if isinstance(decorator, ast.Name) and decorator.id == "map":
|
||||
is_map = True
|
||||
break
|
||||
if is_map:
|
||||
print(f"Found BPF map: {func_node.name}")
|
||||
process_bpf_map(func_node, module)
|
||||
continue
|
||||
return map_sym_tab
|
||||
|
||||
|
||||
BPF_MAP_MAPPINGS = {
|
||||
"HASH": 1, # BPF_MAP_TYPE_HASH
|
||||
"PERF_EVENT_ARRAY": 4, # BPF_MAP_TYPE_PERF_EVENT_ARRAY
|
||||
}
|
||||
|
||||
|
||||
def create_bpf_map(module, map_name, map_params):
|
||||
"""Create a BPF map in the module with the given parameters and debug info"""
|
||||
|
||||
map_type_str = map_params.get("type", "HASH")
|
||||
map_type = BPF_MAP_MAPPINGS.get(map_type_str)
|
||||
|
||||
# Create the anonymous struct type for BPF map
|
||||
map_struct_type = ir.LiteralStructType(
|
||||
[ir.PointerType() for _ in range(len(map_params))])
|
||||
|
||||
# Create the global variable
|
||||
map_global = ir.GlobalVariable(module, map_struct_type, name=map_name)
|
||||
map_global.linkage = 'dso_local'
|
||||
map_global.global_constant = False
|
||||
map_global.initializer = ir.Constant(
|
||||
map_struct_type, None) # type: ignore
|
||||
map_global.section = ".maps"
|
||||
map_global.align = 8 # type: ignore
|
||||
|
||||
# Generate debug info for BTF
|
||||
create_map_debug_info(module, map_global, map_name, map_params)
|
||||
|
||||
print(f"Created BPF map: {map_name}")
|
||||
map_sym_tab[map_name] = map_global
|
||||
return map_global
|
||||
|
||||
|
||||
def create_map_debug_info(module, map_global, map_name, map_params):
|
||||
"""Generate debug information metadata for BPF map"""
|
||||
file_metadata = module._file_metadata
|
||||
compile_unit = module._debug_compile_unit
|
||||
|
||||
# Create basic type for unsigned int (32-bit)
|
||||
uint_type = module.add_debug_info("DIBasicType", {
|
||||
"name": "unsigned int",
|
||||
"size": 32,
|
||||
"encoding": dc.DW_ATE_unsigned
|
||||
})
|
||||
|
||||
# Create basic type for unsigned long long (64-bit)
|
||||
ulong_type = module.add_debug_info("DIBasicType", {
|
||||
"name": "unsigned long long",
|
||||
"size": 64,
|
||||
"encoding": dc.DW_ATE_unsigned
|
||||
})
|
||||
|
||||
# Create array type for map type field (array of 1 unsigned int)
|
||||
array_subrange = module.add_debug_info(
|
||||
"DISubrange", {"count": BPF_MAP_MAPPINGS[map_params.get("type", "HASH")]})
|
||||
array_type = module.add_debug_info("DICompositeType", {
|
||||
"tag": dc.DW_TAG_array_type,
|
||||
"baseType": uint_type,
|
||||
"size": 32,
|
||||
"elements": [array_subrange]
|
||||
})
|
||||
|
||||
# Create pointer types
|
||||
type_ptr = module.add_debug_info("DIDerivedType", {
|
||||
"tag": dc.DW_TAG_pointer_type,
|
||||
"baseType": array_type,
|
||||
"size": 64
|
||||
})
|
||||
|
||||
key_ptr = module.add_debug_info("DIDerivedType", {
|
||||
"tag": dc.DW_TAG_pointer_type,
|
||||
# Adjust based on actual key type
|
||||
"baseType": array_type if "key_size" in map_params else uint_type,
|
||||
"size": 64
|
||||
})
|
||||
|
||||
value_ptr = module.add_debug_info("DIDerivedType", {
|
||||
"tag": dc.DW_TAG_pointer_type,
|
||||
# Adjust based on actual value type
|
||||
"baseType": array_type if "value_size" in map_params else ulong_type,
|
||||
"size": 64
|
||||
})
|
||||
|
||||
elements_arr = []
|
||||
|
||||
# Create struct members
|
||||
# scope field does not appear for some reason
|
||||
cnt = 0
|
||||
for elem in map_params:
|
||||
if elem == "max_entries":
|
||||
continue
|
||||
if elem == "type":
|
||||
ptr = type_ptr
|
||||
elif "key" in elem:
|
||||
ptr = key_ptr
|
||||
else:
|
||||
ptr = value_ptr
|
||||
member = module.add_debug_info("DIDerivedType", {
|
||||
"tag": dc.DW_TAG_member,
|
||||
"name": elem,
|
||||
"file": file_metadata,
|
||||
"baseType": ptr,
|
||||
"size": 64,
|
||||
"offset": cnt * 64
|
||||
})
|
||||
elements_arr.append(member)
|
||||
cnt += 1
|
||||
|
||||
if "max_entries" in map_params:
|
||||
array_subrange_max_entries = module.add_debug_info(
|
||||
"DISubrange", {"count": map_params["max_entries"]})
|
||||
array_type_max_entries = module.add_debug_info("DICompositeType", {
|
||||
"tag": dc.DW_TAG_array_type,
|
||||
"baseType": uint_type,
|
||||
"size": 32,
|
||||
"elements": [array_subrange_max_entries]
|
||||
})
|
||||
max_entries_ptr = module.add_debug_info("DIDerivedType", {
|
||||
"tag": dc.DW_TAG_pointer_type,
|
||||
"baseType": array_type_max_entries,
|
||||
"size": 64
|
||||
})
|
||||
max_entries_member = module.add_debug_info("DIDerivedType", {
|
||||
"tag": dc.DW_TAG_member,
|
||||
"name": "max_entries",
|
||||
"file": file_metadata,
|
||||
"baseType": max_entries_ptr,
|
||||
"size": 64,
|
||||
"offset": cnt * 64
|
||||
})
|
||||
elements_arr.append(max_entries_member)
|
||||
|
||||
# Create the struct type
|
||||
struct_type = module.add_debug_info("DICompositeType", {
|
||||
"tag": dc.DW_TAG_structure_type,
|
||||
"file": file_metadata,
|
||||
"size": 64 * len(elements_arr), # 4 * 64-bit pointers
|
||||
"elements": elements_arr,
|
||||
}, is_distinct=True)
|
||||
|
||||
# Create global variable debug info
|
||||
global_var = module.add_debug_info("DIGlobalVariable", {
|
||||
"name": map_name,
|
||||
"scope": compile_unit,
|
||||
"file": file_metadata,
|
||||
"type": struct_type,
|
||||
"isLocal": False,
|
||||
"isDefinition": True
|
||||
}, is_distinct=True)
|
||||
|
||||
# Create global variable expression
|
||||
global_var_expr = module.add_debug_info("DIGlobalVariableExpression", {
|
||||
"var": global_var,
|
||||
"expr": module.add_debug_info("DIExpression", {})
|
||||
})
|
||||
|
||||
# Attach debug info to the global variable
|
||||
map_global.set_metadata("dbg", global_var_expr)
|
||||
|
||||
return global_var_expr
|
||||
|
||||
|
||||
def process_hash_map(map_name, rval, module):
|
||||
print(f"Creating HashMap map: {map_name}")
|
||||
map_params: dict[str, object] = {"type": "HASH"}
|
||||
|
||||
# Assuming order: key_type, value_type, max_entries
|
||||
if len(rval.args) >= 1 and isinstance(rval.args[0], ast.Name):
|
||||
map_params["key"] = rval.args[0].id
|
||||
if len(rval.args) >= 2 and isinstance(rval.args[1], ast.Name):
|
||||
map_params["value"] = rval.args[1].id
|
||||
if len(rval.args) >= 3 and isinstance(rval.args[2], ast.Constant):
|
||||
const_val = rval.args[2].value
|
||||
if isinstance(const_val, (int, str)): # safe check
|
||||
map_params["max_entries"] = const_val
|
||||
|
||||
for keyword in rval.keywords:
|
||||
if keyword.arg == "key" and isinstance(keyword.value, ast.Name):
|
||||
map_params["key"] = keyword.value.id
|
||||
elif keyword.arg == "value" and isinstance(keyword.value, ast.Name):
|
||||
map_params["value"] = keyword.value.id
|
||||
elif keyword.arg == "max_entries" and isinstance(keyword.value, ast.Constant):
|
||||
const_val = keyword.value.value
|
||||
if isinstance(const_val, (int, str)):
|
||||
map_params["max_entries"] = const_val
|
||||
|
||||
print(f"Map parameters: {map_params}")
|
||||
return create_bpf_map(module, map_name, map_params)
|
||||
|
||||
|
||||
def process_perf_event_map(map_name, rval, module):
|
||||
print(f"Creating PerfEventArray map: {map_name}")
|
||||
map_params = {"type": "PERF_EVENT_ARRAY"}
|
||||
|
||||
if len(rval.args) >= 1 and isinstance(rval.args[0], ast.Name):
|
||||
map_params["key_size"] = rval.args[0].id
|
||||
if len(rval.args) >= 2 and isinstance(rval.args[1], ast.Name):
|
||||
map_params["value_size"] = rval.args[1].id
|
||||
|
||||
for keyword in rval.keywords:
|
||||
if keyword.arg == "key_size" and isinstance(keyword.value, ast.Name):
|
||||
map_params["key_size"] = keyword.value.id
|
||||
elif keyword.arg == "value_size" and isinstance(keyword.value, ast.Name):
|
||||
map_params["value_size"] = keyword.value.id
|
||||
|
||||
print(f"Map parameters: {map_params}")
|
||||
return create_bpf_map(module, map_name, map_params)
|
||||
|
||||
|
||||
def process_bpf_map(func_node, module):
|
||||
"""Process a BPF map (a function decorated with @map)"""
|
||||
map_name = func_node.name
|
||||
print(f"Processing BPF map: {map_name}")
|
||||
|
||||
BPF_MAP_TYPES = {"HashMap": process_hash_map, # BPF_MAP_TYPE_HASH
|
||||
"PerfEventArray": process_perf_event_map, # BPF_MAP_TYPE_PERF_EVENT_ARRAY
|
||||
}
|
||||
|
||||
# For now, assume single return statement
|
||||
return_stmt = None
|
||||
for stmt in func_node.body:
|
||||
if isinstance(stmt, ast.Return):
|
||||
return_stmt = stmt
|
||||
break
|
||||
if return_stmt is None:
|
||||
raise ValueError("BPF map must have a return statement")
|
||||
|
||||
rval = return_stmt.value
|
||||
|
||||
# Handle only HashMap maps
|
||||
if isinstance(rval, ast.Call) and isinstance(rval.func, ast.Name):
|
||||
if rval.func.id in BPF_MAP_TYPES:
|
||||
handler = BPF_MAP_TYPES[rval.func.id]
|
||||
handler(map_name, rval, module)
|
||||
else:
|
||||
print(f"Unknown map type {rval.func.id}, defaulting to HashMap")
|
||||
process_hash_map(map_name, rval, module)
|
||||
else:
|
||||
raise ValueError("Function under @map must return a map")
|
||||
3
pythonbpf/structs/__init__.py
Normal file
3
pythonbpf/structs/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .structs_pass import structs_proc
|
||||
|
||||
__all__ = ["structs_proc"]
|
||||
33
pythonbpf/structs/struct_type.py
Normal file
33
pythonbpf/structs/struct_type.py
Normal file
@ -0,0 +1,33 @@
|
||||
from llvmlite import ir
|
||||
|
||||
|
||||
class StructType:
|
||||
def __init__(self, ir_type, fields, size):
|
||||
self.ir_type = ir_type
|
||||
self.fields = fields
|
||||
self.size = size
|
||||
|
||||
def field_idx(self, field_name):
|
||||
return list(self.fields.keys()).index(field_name)
|
||||
|
||||
def field_type(self, field_name):
|
||||
return self.fields[field_name]
|
||||
|
||||
def gep(self, builder, ptr, field_name):
|
||||
idx = self.field_idx(field_name)
|
||||
return builder.gep(
|
||||
ptr,
|
||||
[ir.Constant(ir.IntType(32), 0), ir.Constant(ir.IntType(32), idx)],
|
||||
inbounds=True,
|
||||
)
|
||||
|
||||
def field_size(self, field_name):
|
||||
fld = self.fields[field_name]
|
||||
if isinstance(fld, ir.ArrayType):
|
||||
return fld.count * (fld.element.width // 8)
|
||||
elif isinstance(fld, ir.IntType):
|
||||
return fld.width // 8
|
||||
elif isinstance(fld, ir.PointerType):
|
||||
return 8
|
||||
|
||||
raise TypeError(f"Unsupported field type: {fld}")
|
||||
95
pythonbpf/structs/structs_pass.py
Normal file
95
pythonbpf/structs/structs_pass.py
Normal file
@ -0,0 +1,95 @@
|
||||
import ast
|
||||
import logging
|
||||
from llvmlite import ir
|
||||
from pythonbpf.type_deducer import ctypes_to_ir
|
||||
from .struct_type import StructType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# TODO: Shall we allow the following syntax:
|
||||
# struct MyStruct:
|
||||
# field1: int
|
||||
# field2: str(32)
|
||||
# Where int is mapped to c_uint64?
|
||||
# Shall we just int64, int32 and uint32 similarly?
|
||||
|
||||
|
||||
def structs_proc(tree, module, chunks):
|
||||
"""Process all class definitions to find BPF structs"""
|
||||
structs_sym_tab = {}
|
||||
for cls_node in chunks:
|
||||
if is_bpf_struct(cls_node):
|
||||
logger.info(f"Found BPF struct: {cls_node.name}")
|
||||
struct_info = process_bpf_struct(cls_node, module)
|
||||
structs_sym_tab[cls_node.name] = struct_info
|
||||
return structs_sym_tab
|
||||
|
||||
|
||||
def is_bpf_struct(cls_node):
|
||||
return any(
|
||||
isinstance(decorator, ast.Name) and decorator.id == "struct"
|
||||
for decorator in cls_node.decorator_list
|
||||
)
|
||||
|
||||
|
||||
def process_bpf_struct(cls_node, module):
|
||||
"""Process a single BPF struct definition"""
|
||||
|
||||
fields = parse_struct_fields(cls_node)
|
||||
field_types = list(fields.values())
|
||||
total_size = calc_struct_size(field_types)
|
||||
struct_type = ir.LiteralStructType(field_types)
|
||||
logger.info(f"Created struct {cls_node.name} with fields {fields.keys()}")
|
||||
return StructType(struct_type, fields, total_size)
|
||||
|
||||
|
||||
def parse_struct_fields(cls_node):
|
||||
"""Parse fields of a struct class node"""
|
||||
fields = {}
|
||||
|
||||
for item in cls_node.body:
|
||||
if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
|
||||
fields[item.target.id] = get_type_from_ann(item.annotation)
|
||||
else:
|
||||
logger.error(f"Unsupported struct field: {ast.dump(item)}")
|
||||
raise TypeError(f"Unsupported field in {ast.dump(cls_node)}")
|
||||
return fields
|
||||
|
||||
|
||||
def get_type_from_ann(annotation):
|
||||
"""Convert an AST annotation node to an LLVM IR type for struct fields"""
|
||||
if isinstance(annotation, ast.Call) and isinstance(annotation.func, ast.Name):
|
||||
if annotation.func.id == "str":
|
||||
# Char array
|
||||
# Assumes constant integer argument
|
||||
length = annotation.args[0].value
|
||||
return ir.ArrayType(ir.IntType(8), length)
|
||||
elif isinstance(annotation, ast.Name):
|
||||
# Int type, written as c_int64, c_uint32, etc.
|
||||
return ctypes_to_ir(annotation.id)
|
||||
|
||||
raise TypeError(f"Unsupported annotation type: {ast.dump(annotation)}")
|
||||
|
||||
|
||||
def calc_struct_size(field_types):
|
||||
"""Calculate total size of the struct with alignment and padding"""
|
||||
curr_offset = 0
|
||||
for ftype in field_types:
|
||||
if isinstance(ftype, ir.IntType):
|
||||
fsize = ftype.width // 8
|
||||
alignment = fsize
|
||||
elif isinstance(ftype, ir.ArrayType):
|
||||
fsize = ftype.count * (ftype.element.width // 8)
|
||||
alignment = ftype.element.width // 8
|
||||
elif isinstance(ftype, ir.PointerType):
|
||||
# We won't encounter this rn, but for the future
|
||||
fsize = 8
|
||||
alignment = 8
|
||||
else:
|
||||
raise TypeError(f"Unsupported field type: {ftype}")
|
||||
|
||||
padding = (alignment - (curr_offset % alignment)) % alignment
|
||||
curr_offset += padding + fsize
|
||||
|
||||
final_padding = (8 - (curr_offset % 8)) % 8
|
||||
return curr_offset + final_padding
|
||||
@ -1,69 +0,0 @@
|
||||
import ast
|
||||
from llvmlite import ir
|
||||
from .type_deducer import ctypes_to_ir
|
||||
from . import dwarf_constants as dc
|
||||
|
||||
structs_sym_tab = {}
|
||||
|
||||
|
||||
def structs_proc(tree, module, chunks):
|
||||
for cls_node in chunks:
|
||||
# Check if this class is a struct
|
||||
is_struct = False
|
||||
for decorator in cls_node.decorator_list:
|
||||
if isinstance(decorator, ast.Name) and decorator.id == "struct":
|
||||
is_struct = True
|
||||
break
|
||||
if is_struct:
|
||||
print(f"Found BPF struct: {cls_node.name}")
|
||||
process_bpf_struct(cls_node, module)
|
||||
continue
|
||||
return structs_sym_tab
|
||||
|
||||
|
||||
def process_bpf_struct(cls_node, module):
|
||||
struct_name = cls_node.name
|
||||
field_names = []
|
||||
field_types = []
|
||||
|
||||
for item in cls_node.body:
|
||||
if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
|
||||
print(f"Field: {item.target.id}, Type: "
|
||||
f"{ast.dump(item.annotation)}")
|
||||
field_names.append(item.target.id)
|
||||
if isinstance(item.annotation, ast.Call) and isinstance(item.annotation.func, ast.Name) and item.annotation.func.id == "str":
|
||||
# This is a char array with fixed length
|
||||
# TODO: For now assuming str is always called with constant
|
||||
field_types.append(ir.ArrayType(
|
||||
ir.IntType(8), item.annotation.args[0].value))
|
||||
else:
|
||||
field_types.append(ctypes_to_ir(item.annotation.id))
|
||||
|
||||
curr_offset = 0
|
||||
for ftype in field_types:
|
||||
if isinstance(ftype, ir.IntType):
|
||||
fsize = ftype.width // 8
|
||||
alignment = fsize
|
||||
elif isinstance(ftype, ir.ArrayType):
|
||||
fsize = ftype.count * (ftype.element.width // 8)
|
||||
alignment = ftype.element.width // 8
|
||||
elif isinstance(ftype, ir.PointerType):
|
||||
fsize = 8
|
||||
alignment = 8
|
||||
else:
|
||||
print(f"Unsupported field type in struct {struct_name}")
|
||||
return
|
||||
padding = (alignment - (curr_offset % alignment)) % alignment
|
||||
curr_offset += padding
|
||||
curr_offset += fsize
|
||||
final_padding = (8 - (curr_offset % 8)) % 8
|
||||
total_size = curr_offset + final_padding
|
||||
|
||||
struct_type = ir.LiteralStructType(field_types)
|
||||
structs_sym_tab[struct_name] = {
|
||||
"type": struct_type,
|
||||
"fields": {name: idx for idx, name in enumerate(field_names)},
|
||||
"size": total_size,
|
||||
"field_types": field_types,
|
||||
}
|
||||
print(f"Created struct {struct_name} with fields {field_names}")
|
||||
@ -1,54 +0,0 @@
|
||||
class TraceEvent:
|
||||
def __init__(self, timestamp, comm, pid, cpu, flags, message):
|
||||
"""Represents a parsed trace pipe event"""
|
||||
self.timestamp = timestamp # float: timestamp in seconds
|
||||
self.comm = comm # str: command name
|
||||
self.pid = pid # int: process ID
|
||||
self.cpu = cpu # int: CPU number
|
||||
self.flags = flags # str: trace flags
|
||||
self.message = message # str: the actual message
|
||||
|
||||
def __iter__(self):
|
||||
"""Allow unpacking like the original BCC tuple"""
|
||||
yield self.comm
|
||||
yield self.pid
|
||||
yield self.cpu
|
||||
yield self.flags
|
||||
yield self.timestamp
|
||||
yield self.message
|
||||
|
||||
|
||||
class TraceReader:
|
||||
def __init__(self, trace_pipe_path="/sys/kernel/debug/tracing/trace_pipe"):
|
||||
self.trace_pipe_path = trace_pipe_path
|
||||
self.file = None
|
||||
|
||||
def __enter__(self):
|
||||
self.file = open(self.trace_pipe_path, "r")
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if self.file:
|
||||
self.file.close()
|
||||
|
||||
def __iter__(self):
|
||||
while True:
|
||||
event = self.trace_fields()
|
||||
if event:
|
||||
yield event
|
||||
|
||||
def trace_fields(self):
|
||||
"""Read and parse one line from the trace pipe"""
|
||||
if not self.file:
|
||||
self.file = open(self.trace_pipe_path, "r")
|
||||
line = self.file.readline()
|
||||
if not line:
|
||||
return None
|
||||
# Parse the line into components (simplified)
|
||||
# Real implementation would need more robust parsing
|
||||
parts = self._parse_trace_line(line)
|
||||
return TraceEvent(*parts)
|
||||
|
||||
def _parse_trace_line(self, line):
|
||||
# TODO: Implement
|
||||
pass
|
||||
@ -1,24 +1,28 @@
|
||||
from llvmlite import ir
|
||||
|
||||
# TODO: THIS IS NOT SUPPOSED TO MATCH STRINGS :skull:
|
||||
mapping = {
|
||||
"c_int8": ir.IntType(8),
|
||||
"c_uint8": ir.IntType(8),
|
||||
"c_int16": ir.IntType(16),
|
||||
"c_uint16": ir.IntType(16),
|
||||
"c_int32": ir.IntType(32),
|
||||
"c_uint32": ir.IntType(32),
|
||||
"c_int64": ir.IntType(64),
|
||||
"c_uint64": ir.IntType(64),
|
||||
"c_float": ir.FloatType(),
|
||||
"c_double": ir.DoubleType(),
|
||||
"c_void_p": ir.IntType(64),
|
||||
# Not so sure about this one
|
||||
"str": ir.PointerType(ir.IntType(8)),
|
||||
}
|
||||
|
||||
|
||||
def ctypes_to_ir(ctype: str):
|
||||
mapping = {
|
||||
"c_int8": ir.IntType(8),
|
||||
"c_uint8": ir.IntType(8),
|
||||
"c_int16": ir.IntType(16),
|
||||
"c_uint16": ir.IntType(16),
|
||||
"c_int32": ir.IntType(32),
|
||||
"c_uint32": ir.IntType(32),
|
||||
"c_int64": ir.IntType(64),
|
||||
"c_uint64": ir.IntType(64),
|
||||
"c_float": ir.FloatType(),
|
||||
"c_double": ir.DoubleType(),
|
||||
"c_void_p": ir.IntType(64),
|
||||
# Not so sure about this one
|
||||
"str": ir.PointerType(ir.IntType(8))
|
||||
}
|
||||
if ctype in mapping:
|
||||
return mapping[ctype]
|
||||
raise NotImplementedError(f"No mapping for {ctype}")
|
||||
|
||||
|
||||
def is_ctypes(ctype: str) -> bool:
|
||||
return ctype in mapping
|
||||
|
||||
3
pythonbpf/vmlinux_parser/__init__.py
Normal file
3
pythonbpf/vmlinux_parser/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .import_detector import vmlinux_proc
|
||||
|
||||
__all__ = ["vmlinux_proc"]
|
||||
205
pythonbpf/vmlinux_parser/class_handler.py
Normal file
205
pythonbpf/vmlinux_parser/class_handler.py
Normal file
@ -0,0 +1,205 @@
|
||||
import logging
|
||||
from functools import lru_cache
|
||||
import importlib
|
||||
from .dependency_handler import DependencyHandler
|
||||
from .dependency_node import DependencyNode
|
||||
import ctypes
|
||||
from typing import Optional, Any, Dict
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_module_symbols(module_name: str):
|
||||
imported_module = importlib.import_module(module_name)
|
||||
return [name for name in dir(imported_module)], imported_module
|
||||
|
||||
|
||||
def process_vmlinux_class(node, llvm_module, handler: DependencyHandler):
|
||||
symbols_in_module, imported_module = get_module_symbols("vmlinux")
|
||||
if node.name in symbols_in_module:
|
||||
vmlinux_type = getattr(imported_module, node.name)
|
||||
process_vmlinux_post_ast(vmlinux_type, llvm_module, handler)
|
||||
else:
|
||||
raise ImportError(f"{node.name} not in vmlinux")
|
||||
|
||||
|
||||
def process_vmlinux_post_ast(
|
||||
elem_type_class, llvm_handler, handler: DependencyHandler, processing_stack=None
|
||||
):
|
||||
# Initialize processing stack on first call
|
||||
if processing_stack is None:
|
||||
processing_stack = set()
|
||||
symbols_in_module, imported_module = get_module_symbols("vmlinux")
|
||||
|
||||
current_symbol_name = elem_type_class.__name__
|
||||
logger.info(f"Begin {current_symbol_name} Processing")
|
||||
field_table: Dict[str, list] = {}
|
||||
is_complex_type = False
|
||||
containing_type: Optional[Any] = None
|
||||
ctype_complex_type: Optional[Any] = None
|
||||
type_length: Optional[int] = None
|
||||
module_name = getattr(elem_type_class, "__module__", None)
|
||||
|
||||
# Check if already processed
|
||||
if handler.has_node(current_symbol_name):
|
||||
logger.debug(f"Node {current_symbol_name} already processed and ready")
|
||||
return True
|
||||
|
||||
# XXX:Check it's use. It's probably not being used.
|
||||
if current_symbol_name in processing_stack:
|
||||
logger.debug(
|
||||
f"Dependency already in processing stack for {current_symbol_name}, skipping"
|
||||
)
|
||||
return True
|
||||
|
||||
processing_stack.add(current_symbol_name)
|
||||
|
||||
if module_name == "vmlinux":
|
||||
if hasattr(elem_type_class, "_type_"):
|
||||
pass
|
||||
else:
|
||||
new_dep_node = DependencyNode(name=current_symbol_name)
|
||||
|
||||
# elem_type_class is the actual vmlinux struct/class
|
||||
new_dep_node.set_ctype_struct(elem_type_class)
|
||||
|
||||
handler.add_node(new_dep_node)
|
||||
class_obj = getattr(imported_module, current_symbol_name)
|
||||
# Inspect the class fields
|
||||
if hasattr(class_obj, "_fields_"):
|
||||
for field_elem in class_obj._fields_:
|
||||
field_name: str = ""
|
||||
field_type: Optional[Any] = None
|
||||
bitfield_size: Optional[int] = None
|
||||
if len(field_elem) == 2:
|
||||
field_name, field_type = field_elem
|
||||
elif len(field_elem) == 3:
|
||||
field_name, field_type, bitfield_size = field_elem
|
||||
field_table[field_name] = [field_type, bitfield_size]
|
||||
elif hasattr(class_obj, "__annotations__"):
|
||||
for field_elem in class_obj.__annotations__.items():
|
||||
if len(field_elem) == 2:
|
||||
field_name, field_type = field_elem
|
||||
bitfield_size = None
|
||||
elif len(field_elem) == 3:
|
||||
field_name, field_type, bitfield_size = field_elem
|
||||
else:
|
||||
raise ValueError(
|
||||
"Number of fields in items() of class object unexpected"
|
||||
)
|
||||
field_table[field_name] = [field_type, bitfield_size]
|
||||
else:
|
||||
raise TypeError("Could not get required class and definition")
|
||||
|
||||
logger.debug(f"Extracted fields for {current_symbol_name}: {field_table}")
|
||||
for elem in field_table.items():
|
||||
elem_name, elem_temp_list = elem
|
||||
[elem_type, elem_bitfield_size] = elem_temp_list
|
||||
local_module_name = getattr(elem_type, "__module__", None)
|
||||
new_dep_node.add_field(elem_name, elem_type, ready=False)
|
||||
if local_module_name == ctypes.__name__:
|
||||
new_dep_node.set_field_bitfield_size(elem_name, elem_bitfield_size)
|
||||
new_dep_node.set_field_ready(elem_name, is_ready=True)
|
||||
logger.debug(
|
||||
f"Field {elem_name} is direct ctypes type: {elem_type}"
|
||||
)
|
||||
elif local_module_name == "vmlinux":
|
||||
new_dep_node.set_field_bitfield_size(elem_name, elem_bitfield_size)
|
||||
logger.debug(
|
||||
f"Processing vmlinux field: {elem_name}, type: {elem_type}"
|
||||
)
|
||||
if hasattr(elem_type, "_type_"):
|
||||
is_complex_type = True
|
||||
containing_type = elem_type._type_
|
||||
if hasattr(elem_type, "_length_") and is_complex_type:
|
||||
type_length = elem_type._length_
|
||||
|
||||
if containing_type.__module__ == "vmlinux":
|
||||
new_dep_node.add_dependent(
|
||||
elem_type._type_.__name__
|
||||
if hasattr(elem_type._type_, "__name__")
|
||||
else str(elem_type._type_)
|
||||
)
|
||||
elif containing_type.__module__ == ctypes.__name__:
|
||||
if isinstance(elem_type, type):
|
||||
if issubclass(elem_type, ctypes.Array):
|
||||
ctype_complex_type = ctypes.Array
|
||||
elif issubclass(elem_type, ctypes._Pointer):
|
||||
ctype_complex_type = ctypes._Pointer
|
||||
else:
|
||||
raise TypeError("Unsupported ctypes subclass")
|
||||
else:
|
||||
raise ImportError(
|
||||
f"Unsupported module of {containing_type}"
|
||||
)
|
||||
logger.debug(
|
||||
f"{containing_type} containing type of parent {elem_name} with {elem_type} and ctype {ctype_complex_type} and length {type_length}"
|
||||
)
|
||||
new_dep_node.set_field_containing_type(
|
||||
elem_name, containing_type
|
||||
)
|
||||
new_dep_node.set_field_type_size(elem_name, type_length)
|
||||
new_dep_node.set_field_ctype_complex_type(
|
||||
elem_name, ctype_complex_type
|
||||
)
|
||||
new_dep_node.set_field_type(elem_name, elem_type)
|
||||
if containing_type.__module__ == "vmlinux":
|
||||
containing_type_name = (
|
||||
containing_type.__name__
|
||||
if hasattr(containing_type, "__name__")
|
||||
else str(containing_type)
|
||||
)
|
||||
|
||||
# Check for self-reference or already processed
|
||||
if containing_type_name == current_symbol_name:
|
||||
# Self-referential pointer
|
||||
logger.debug(
|
||||
f"Self-referential pointer in {current_symbol_name}.{elem_name}"
|
||||
)
|
||||
new_dep_node.set_field_ready(elem_name, True)
|
||||
elif handler.has_node(containing_type_name):
|
||||
# Already processed
|
||||
logger.debug(
|
||||
f"Reusing already processed {containing_type_name}"
|
||||
)
|
||||
new_dep_node.set_field_ready(elem_name, True)
|
||||
else:
|
||||
# Process recursively - THIS WAS MISSING
|
||||
new_dep_node.add_dependent(containing_type_name)
|
||||
process_vmlinux_post_ast(
|
||||
containing_type,
|
||||
llvm_handler,
|
||||
handler,
|
||||
processing_stack,
|
||||
)
|
||||
new_dep_node.set_field_ready(elem_name, True)
|
||||
elif containing_type.__module__ == ctypes.__name__:
|
||||
logger.debug(f"Processing ctype internal{containing_type}")
|
||||
new_dep_node.set_field_ready(elem_name, True)
|
||||
else:
|
||||
raise TypeError(
|
||||
"Module not supported in recursive resolution"
|
||||
)
|
||||
else:
|
||||
new_dep_node.add_dependent(
|
||||
elem_type.__name__
|
||||
if hasattr(elem_type, "__name__")
|
||||
else str(elem_type)
|
||||
)
|
||||
process_vmlinux_post_ast(
|
||||
elem_type, llvm_handler, handler, processing_stack
|
||||
)
|
||||
new_dep_node.set_field_ready(elem_name, True)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"{elem_name} with type {elem_type} from module {module_name} not supported in recursive resolver"
|
||||
)
|
||||
|
||||
else:
|
||||
raise ImportError("UNSUPPORTED Module")
|
||||
|
||||
logging.info(
|
||||
f"{current_symbol_name} processed and handler readiness {handler.is_ready}"
|
||||
)
|
||||
return True
|
||||
173
pythonbpf/vmlinux_parser/dependency_handler.py
Normal file
173
pythonbpf/vmlinux_parser/dependency_handler.py
Normal file
@ -0,0 +1,173 @@
|
||||
from typing import Optional, Dict, List, Iterator
|
||||
from .dependency_node import DependencyNode
|
||||
|
||||
|
||||
class DependencyHandler:
|
||||
"""
|
||||
Manages a collection of DependencyNode objects with no duplicates.
|
||||
|
||||
Ensures that no two nodes with the same name can be added and provides
|
||||
methods to check readiness and retrieve specific nodes.
|
||||
|
||||
Example usage:
|
||||
# Create a handler
|
||||
handler = DependencyHandler()
|
||||
|
||||
# Create some dependency nodes
|
||||
node1 = DependencyNode(name="node1")
|
||||
node1.add_field("field1", str)
|
||||
node1.set_field_value("field1", "value1")
|
||||
|
||||
node2 = DependencyNode(name="node2")
|
||||
node2.add_field("field1", int)
|
||||
|
||||
# Add nodes to the handler
|
||||
handler.add_node(node1)
|
||||
handler.add_node(node2)
|
||||
|
||||
# Check if a specific node exists
|
||||
print(handler.has_node("node1")) # True
|
||||
|
||||
# Get a reference to a node and modify it
|
||||
node = handler.get_node("node2")
|
||||
node.set_field_value("field1", 42)
|
||||
|
||||
# Check if all nodes are ready
|
||||
print(handler.is_ready) # False (node2 is ready, but node1 isn't)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# Using a dictionary with node names as keys ensures name uniqueness
|
||||
# and provides efficient lookups
|
||||
self._nodes: Dict[str, DependencyNode] = {}
|
||||
|
||||
def add_node(self, node: DependencyNode) -> bool:
|
||||
"""
|
||||
Add a dependency node to the handler.
|
||||
|
||||
Args:
|
||||
node: The DependencyNode to add
|
||||
|
||||
Returns:
|
||||
bool: True if the node was added, False if a node with the same name already exists
|
||||
|
||||
Raises:
|
||||
TypeError: If the provided object is not a DependencyNode
|
||||
"""
|
||||
if not isinstance(node, DependencyNode):
|
||||
raise TypeError(f"Expected DependencyNode, got {type(node).__name__}")
|
||||
|
||||
# Check if a node with this name already exists
|
||||
if node.name in self._nodes:
|
||||
return False
|
||||
|
||||
self._nodes[node.name] = node
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_ready(self) -> bool:
|
||||
"""
|
||||
Check if all nodes are ready.
|
||||
|
||||
Returns:
|
||||
bool: True if all nodes are ready (or if there are no nodes), False otherwise
|
||||
"""
|
||||
if not self._nodes:
|
||||
return True
|
||||
|
||||
return all(node.is_ready for node in self._nodes.values())
|
||||
|
||||
def has_node(self, name: str) -> bool:
|
||||
"""
|
||||
Check if a node with the given name exists.
|
||||
|
||||
Args:
|
||||
name: The name to check
|
||||
|
||||
Returns:
|
||||
bool: True if a node with the given name exists, False otherwise
|
||||
"""
|
||||
return name in self._nodes
|
||||
|
||||
def get_node(self, name: str) -> Optional[DependencyNode]:
|
||||
"""
|
||||
Get a node by name for manipulation.
|
||||
|
||||
Args:
|
||||
name: The name of the node to retrieve
|
||||
|
||||
Returns:
|
||||
Optional[DependencyNode]: The node with the given name, or None if not found
|
||||
"""
|
||||
return self._nodes.get(name)
|
||||
|
||||
def remove_node(self, node_or_name) -> bool:
|
||||
"""
|
||||
Remove a node by name or reference.
|
||||
|
||||
Args:
|
||||
node_or_name: The node to remove or its name
|
||||
|
||||
Returns:
|
||||
bool: True if the node was removed, False if not found
|
||||
"""
|
||||
if isinstance(node_or_name, DependencyNode):
|
||||
name = node_or_name.name
|
||||
else:
|
||||
name = node_or_name
|
||||
|
||||
if name in self._nodes:
|
||||
del self._nodes[name]
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_all_nodes(self) -> List[DependencyNode]:
|
||||
"""
|
||||
Get all nodes stored in the handler.
|
||||
|
||||
Returns:
|
||||
List[DependencyNode]: List of all nodes
|
||||
"""
|
||||
return list(self._nodes.values())
|
||||
|
||||
def __iter__(self) -> Iterator[DependencyNode]:
|
||||
"""
|
||||
Iterate over all nodes.
|
||||
|
||||
Returns:
|
||||
Iterator[DependencyNode]: Iterator over all nodes
|
||||
"""
|
||||
return iter(self._nodes.values())
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""
|
||||
Get the number of nodes in the handler.
|
||||
|
||||
Returns:
|
||||
int: The number of nodes
|
||||
"""
|
||||
return len(self._nodes)
|
||||
|
||||
def __getitem__(self, name: str) -> DependencyNode:
|
||||
"""
|
||||
Get a node by name using dictionary-style access.
|
||||
|
||||
Args:
|
||||
name: The name of the node to retrieve
|
||||
|
||||
Returns:
|
||||
DependencyNode: The node with the given name
|
||||
|
||||
Raises:
|
||||
KeyError: If no node with the given name exists
|
||||
|
||||
Example:
|
||||
node = handler["some-dep_node_name"]
|
||||
"""
|
||||
if name not in self._nodes:
|
||||
raise KeyError(f"No node with name '{name}' found")
|
||||
return self._nodes[name]
|
||||
|
||||
@property
|
||||
def nodes(self):
|
||||
return self._nodes
|
||||
363
pythonbpf/vmlinux_parser/dependency_node.py
Normal file
363
pythonbpf/vmlinux_parser/dependency_node.py
Normal file
@ -0,0 +1,363 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Any, Optional
|
||||
import ctypes
|
||||
|
||||
|
||||
# TODO: FIX THE FUCKING TYPE NAME CONVENTION.
|
||||
@dataclass
|
||||
class Field:
|
||||
"""Represents a field in a dependency node with its type and readiness state."""
|
||||
|
||||
name: str
|
||||
type: type
|
||||
ctype_complex_type: Optional[Any]
|
||||
containing_type: Optional[Any]
|
||||
type_size: Optional[int]
|
||||
bitfield_size: Optional[int]
|
||||
offset: int
|
||||
value: Any = None
|
||||
ready: bool = False
|
||||
|
||||
def set_ready(self, is_ready: bool = True) -> None:
|
||||
"""Set the readiness state of this field."""
|
||||
self.ready = is_ready
|
||||
|
||||
def set_value(self, value: Any, mark_ready: bool = False) -> None:
|
||||
"""Set the value of this field and optionally mark it as ready."""
|
||||
self.value = value
|
||||
if mark_ready:
|
||||
self.ready = True
|
||||
|
||||
def set_type(self, given_type, mark_ready: bool = False) -> None:
|
||||
"""Set value of the type field and mark as ready"""
|
||||
self.type = given_type
|
||||
if mark_ready:
|
||||
self.ready = True
|
||||
|
||||
def set_containing_type(
|
||||
self, containing_type: Optional[Any], mark_ready: bool = False
|
||||
) -> None:
|
||||
"""Set the containing_type of this field and optionally mark it as ready."""
|
||||
self.containing_type = containing_type
|
||||
if mark_ready:
|
||||
self.ready = True
|
||||
|
||||
def set_type_size(self, type_size: Any, mark_ready: bool = False) -> None:
|
||||
"""Set the type_size of this field and optionally mark it as ready."""
|
||||
self.type_size = type_size
|
||||
if mark_ready:
|
||||
self.ready = True
|
||||
|
||||
def set_ctype_complex_type(
|
||||
self, ctype_complex_type: Any, mark_ready: bool = False
|
||||
) -> None:
|
||||
"""Set the ctype_complex_type of this field and optionally mark it as ready."""
|
||||
self.ctype_complex_type = ctype_complex_type
|
||||
if mark_ready:
|
||||
self.ready = True
|
||||
|
||||
def set_bitfield_size(self, bitfield_size: Any, mark_ready: bool = False) -> None:
|
||||
"""Set the bitfield_size of this field and optionally mark it as ready."""
|
||||
self.bitfield_size = bitfield_size
|
||||
if mark_ready:
|
||||
self.ready = True
|
||||
|
||||
def set_offset(self, offset: int) -> None:
|
||||
"""Set the offset of this field"""
|
||||
self.offset = offset
|
||||
|
||||
|
||||
@dataclass
|
||||
class DependencyNode:
|
||||
"""
|
||||
A node with typed fields and readiness tracking.
|
||||
|
||||
Example usage:
|
||||
# Create a dependency node for a Person
|
||||
somestruct = DependencyNode(name="struct_1")
|
||||
|
||||
# Add fields with their types
|
||||
somestruct.add_field("field_1", str)
|
||||
somestruct.add_field("field_2", int)
|
||||
somestruct.add_field("field_3", str)
|
||||
|
||||
# Check if the node is ready (should be False initially)
|
||||
print(f"Is node ready? {somestruct.is_ready}") # False
|
||||
|
||||
# Set some field values
|
||||
somestruct.set_field_value("field_1", "someproperty")
|
||||
somestruct.set_field_value("field_2", 30)
|
||||
|
||||
# Check if the node is ready (still False because email is not ready)
|
||||
print(f"Is node ready? {somestruct.is_ready}") # False
|
||||
|
||||
# Set the last field and make the node ready
|
||||
somestruct.set_field_value("field_3", "anotherproperty")
|
||||
|
||||
# Now the node should be ready
|
||||
print(f"Is node ready? {somestruct.is_ready}") # True
|
||||
|
||||
# You can also mark a field as not ready
|
||||
somestruct.set_field_ready("field_3", False)
|
||||
|
||||
# Now the node is not ready again
|
||||
print(f"Is node ready? {somestruct.is_ready}") # False
|
||||
|
||||
# Get all field values
|
||||
print(somestruct.get_field_values()) # {'field_1': 'someproperty', 'field_2': 30, 'field_3': 'anotherproperty'}
|
||||
|
||||
# Get only ready fields
|
||||
ready_fields = somestruct.get_ready_fields()
|
||||
print(f"Ready fields: {[field.name for field in ready_fields.values()]}") # ['field_1', 'field_2']
|
||||
"""
|
||||
|
||||
name: str
|
||||
depends_on: Optional[list[str]] = None
|
||||
fields: Dict[str, Field] = field(default_factory=dict)
|
||||
_ready_cache: Optional[bool] = field(default=None, repr=False)
|
||||
current_offset: int = 0
|
||||
ctype_struct: Optional[Any] = field(default=None, repr=False)
|
||||
|
||||
def add_field(
|
||||
self,
|
||||
name: str,
|
||||
field_type: type,
|
||||
initial_value: Any = None,
|
||||
containing_type: Optional[Any] = None,
|
||||
type_size: Optional[int] = None,
|
||||
ctype_complex_type: Optional[int] = None,
|
||||
bitfield_size: Optional[int] = None,
|
||||
ready: bool = False,
|
||||
offset: int = 0,
|
||||
) -> None:
|
||||
"""Add a field to the node with an optional initial value and readiness state."""
|
||||
if self.depends_on is None:
|
||||
self.depends_on = []
|
||||
self.fields[name] = Field(
|
||||
name=name,
|
||||
type=field_type,
|
||||
value=initial_value,
|
||||
ready=ready,
|
||||
containing_type=containing_type,
|
||||
type_size=type_size,
|
||||
ctype_complex_type=ctype_complex_type,
|
||||
bitfield_size=bitfield_size,
|
||||
offset=offset,
|
||||
)
|
||||
# Invalidate readiness cache
|
||||
self._ready_cache = None
|
||||
|
||||
def set_ctype_struct(self, ctype_struct: Any) -> None:
|
||||
"""Set the ctypes structure for automatic offset calculation."""
|
||||
self.ctype_struct = ctype_struct
|
||||
|
||||
def __sizeof__(self):
|
||||
# If we have a ctype_struct, use its size
|
||||
if self.ctype_struct is not None:
|
||||
return ctypes.sizeof(self.ctype_struct)
|
||||
return self.current_offset
|
||||
|
||||
def get_field(self, name: str) -> Field:
|
||||
"""Get a field by name."""
|
||||
return self.fields[name]
|
||||
|
||||
def set_field_value(self, name: str, value: Any, mark_ready: bool = False) -> None:
|
||||
"""Set a field's value and optionally mark it as ready."""
|
||||
if name not in self.fields:
|
||||
raise KeyError(f"Field '{name}' does not exist in node '{self.name}'")
|
||||
|
||||
self.fields[name].set_value(value, mark_ready)
|
||||
# Invalidate readiness cache
|
||||
self._ready_cache = None
|
||||
|
||||
def set_field_type(self, name: str, type: Any, mark_ready: bool = False) -> None:
|
||||
"""Set a field's type and optionally mark it as ready."""
|
||||
if name not in self.fields:
|
||||
raise KeyError(f"Field '{name}' does not exist in node '{self.name}'")
|
||||
|
||||
self.fields[name].set_type(type, mark_ready)
|
||||
# Invalidate readiness cache
|
||||
self._ready_cache = None
|
||||
|
||||
def set_field_containing_type(
|
||||
self, name: str, containing_type: Any, mark_ready: bool = False
|
||||
) -> None:
|
||||
"""Set a field's containing_type and optionally mark it as ready."""
|
||||
if name not in self.fields:
|
||||
raise KeyError(f"Field '{name}' does not exist in node '{self.name}'")
|
||||
|
||||
self.fields[name].set_containing_type(containing_type, mark_ready)
|
||||
# Invalidate readiness cache
|
||||
self._ready_cache = None
|
||||
|
||||
def set_field_type_size(
|
||||
self, name: str, type_size: Any, mark_ready: bool = False
|
||||
) -> None:
|
||||
"""Set a field's type_size and optionally mark it as ready."""
|
||||
if name not in self.fields:
|
||||
raise KeyError(f"Field '{name}' does not exist in node '{self.name}'")
|
||||
|
||||
self.fields[name].set_type_size(type_size, mark_ready)
|
||||
# Invalidate readiness cache
|
||||
self._ready_cache = None
|
||||
|
||||
def set_field_ctype_complex_type(
|
||||
self, name: str, ctype_complex_type: Any, mark_ready: bool = False
|
||||
) -> None:
|
||||
"""Set a field's ctype_complex_type and optionally mark it as ready."""
|
||||
if name not in self.fields:
|
||||
raise KeyError(f"Field '{name}' does not exist in node '{self.name}'")
|
||||
|
||||
self.fields[name].set_ctype_complex_type(ctype_complex_type, mark_ready)
|
||||
# Invalidate readiness cache
|
||||
self._ready_cache = None
|
||||
|
||||
def set_field_bitfield_size(
|
||||
self, name: str, bitfield_size: Any, mark_ready: bool = False
|
||||
) -> None:
|
||||
"""Set a field's bitfield_size and optionally mark it as ready."""
|
||||
if name not in self.fields:
|
||||
raise KeyError(f"Field '{name}' does not exist in node '{self.name}'")
|
||||
|
||||
self.fields[name].set_bitfield_size(bitfield_size, mark_ready)
|
||||
# Invalidate readiness cache
|
||||
self._ready_cache = None
|
||||
|
||||
def set_field_ready(
|
||||
self,
|
||||
name: str,
|
||||
is_ready: bool = False,
|
||||
size_of_containing_type: Optional[int] = None,
|
||||
) -> None:
|
||||
"""Mark a field as ready or not ready."""
|
||||
if name not in self.fields:
|
||||
raise KeyError(f"Field '{name}' does not exist in node '{self.name}'")
|
||||
|
||||
self.fields[name].set_ready(is_ready)
|
||||
|
||||
# Use ctypes built-in offset if available
|
||||
if self.ctype_struct is not None:
|
||||
try:
|
||||
self.fields[name].set_offset(getattr(self.ctype_struct, name).offset)
|
||||
except AttributeError:
|
||||
# Fallback to manual calculation if field not found in ctype_struct
|
||||
self.fields[name].set_offset(self.current_offset)
|
||||
self.current_offset += self._calculate_size(
|
||||
name, size_of_containing_type
|
||||
)
|
||||
else:
|
||||
# Manual offset calculation when no ctype_struct is available
|
||||
self.fields[name].set_offset(self.current_offset)
|
||||
self.current_offset += self._calculate_size(name, size_of_containing_type)
|
||||
|
||||
# Invalidate readiness cache
|
||||
self._ready_cache = None
|
||||
|
||||
def _calculate_size(
|
||||
self, name: str, size_of_containing_type: Optional[int] = None
|
||||
) -> int:
|
||||
processing_field = self.fields[name]
|
||||
# size_of_field will be in bytes
|
||||
if processing_field.type.__module__ == ctypes.__name__:
|
||||
size_of_field = ctypes.sizeof(processing_field.type)
|
||||
return size_of_field
|
||||
elif processing_field.type.__module__ == "vmlinux":
|
||||
if processing_field.ctype_complex_type is not None:
|
||||
if issubclass(processing_field.ctype_complex_type, ctypes.Array):
|
||||
if processing_field.containing_type.__module__ == ctypes.__name__:
|
||||
if (
|
||||
processing_field.containing_type is not None
|
||||
and processing_field.type_size is not None
|
||||
):
|
||||
size_of_field = (
|
||||
ctypes.sizeof(processing_field.containing_type)
|
||||
* processing_field.type_size
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"{processing_field} has no containing_type or type_size"
|
||||
)
|
||||
return size_of_field
|
||||
elif processing_field.containing_type.__module__ == "vmlinux":
|
||||
if (
|
||||
size_of_containing_type is not None
|
||||
and processing_field.type_size is not None
|
||||
):
|
||||
size_of_field = (
|
||||
size_of_containing_type * processing_field.type_size
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"{processing_field} has no containing_type or type_size"
|
||||
)
|
||||
return size_of_field
|
||||
elif issubclass(processing_field.ctype_complex_type, ctypes._Pointer):
|
||||
return ctypes.sizeof(ctypes.c_void_p)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"This subclass of ctype not supported yet"
|
||||
)
|
||||
elif processing_field.type_size is not None:
|
||||
# Handle vmlinux types with type_size but no ctype_complex_type
|
||||
# This means it's a direct vmlinux struct field (not array/pointer wrapped)
|
||||
# The type_size should already contain the full size of the struct
|
||||
# But if there's a containing_type from vmlinux, we need that size
|
||||
if processing_field.containing_type is not None:
|
||||
if processing_field.containing_type.__module__ == "vmlinux":
|
||||
# For vmlinux containing types, we need the pre-calculated size
|
||||
if size_of_containing_type is not None:
|
||||
return size_of_containing_type * processing_field.type_size
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"Field {name}: vmlinux containing_type requires size_of_containing_type"
|
||||
)
|
||||
else:
|
||||
raise ModuleNotFoundError(
|
||||
f"Containing type module {processing_field.containing_type.__module__} not supported"
|
||||
)
|
||||
else:
|
||||
raise RuntimeError("Wrong type found with no containing type")
|
||||
else:
|
||||
# No ctype_complex_type and no type_size, must rely on size_of_containing_type
|
||||
if size_of_containing_type is None:
|
||||
raise RuntimeError(
|
||||
f"Size of containing type {size_of_containing_type} is None"
|
||||
)
|
||||
return size_of_containing_type
|
||||
|
||||
else:
|
||||
raise ModuleNotFoundError("Module is not supported for the operation")
|
||||
raise RuntimeError("control should not reach here")
|
||||
|
||||
@property
|
||||
def is_ready(self) -> bool:
|
||||
"""Check if the node is ready (all fields are ready)."""
|
||||
# Use cached value if available
|
||||
if self._ready_cache is not None:
|
||||
return self._ready_cache
|
||||
|
||||
# Calculate readiness only when needed
|
||||
if not self.fields:
|
||||
self._ready_cache = True
|
||||
return True
|
||||
|
||||
self._ready_cache = all(elem.ready for elem in self.fields.values())
|
||||
return self._ready_cache
|
||||
|
||||
def get_field_values(self) -> Dict[str, Any]:
|
||||
"""Get a dictionary of field names to their values."""
|
||||
return {name: elem.value for name, elem in self.fields.items()}
|
||||
|
||||
def get_ready_fields(self) -> Dict[str, Field]:
|
||||
"""Get all fields that are marked as ready."""
|
||||
return {name: elem for name, elem in self.fields.items() if elem.ready}
|
||||
|
||||
def get_not_ready_fields(self) -> Dict[str, Field]:
|
||||
"""Get all fields that are marked as not ready."""
|
||||
return {name: elem for name, elem in self.fields.items() if not elem.ready}
|
||||
|
||||
def add_dependent(self, dep_type):
|
||||
if dep_type in self.depends_on:
|
||||
return
|
||||
else:
|
||||
self.depends_on.append(dep_type)
|
||||
147
pythonbpf/vmlinux_parser/import_detector.py
Normal file
147
pythonbpf/vmlinux_parser/import_detector.py
Normal file
@ -0,0 +1,147 @@
|
||||
import ast
|
||||
import logging
|
||||
from typing import List, Tuple, Any
|
||||
import importlib
|
||||
import inspect
|
||||
|
||||
from .dependency_handler import DependencyHandler
|
||||
from .ir_gen import IRGenerator
|
||||
from .class_handler import process_vmlinux_class
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def detect_import_statement(tree: ast.AST) -> List[Tuple[str, ast.ImportFrom]]:
|
||||
"""
|
||||
Parse AST and detect import statements from vmlinux.
|
||||
|
||||
Returns a list of tuples (module_name, imported_item) for vmlinux imports.
|
||||
Raises SyntaxError for invalid import patterns.
|
||||
|
||||
Args:
|
||||
tree: The AST to parse
|
||||
|
||||
Returns:
|
||||
List of tuples containing (module_name, imported_item) for each vmlinux import
|
||||
|
||||
Raises:
|
||||
SyntaxError: If multiple imports from vmlinux are attempted or import * is used
|
||||
"""
|
||||
vmlinux_imports = []
|
||||
|
||||
for node in ast.walk(tree):
|
||||
# Handle "from vmlinux import ..." statements
|
||||
if isinstance(node, ast.ImportFrom):
|
||||
if node.module == "vmlinux":
|
||||
# Check for wildcard import: from vmlinux import *
|
||||
if any(alias.name == "*" for alias in node.names):
|
||||
raise SyntaxError(
|
||||
"Wildcard imports from vmlinux are not supported. "
|
||||
"Please import specific types explicitly."
|
||||
)
|
||||
|
||||
# Check for multiple imports: from vmlinux import A, B, C
|
||||
if len(node.names) > 1:
|
||||
imported_names = [alias.name for alias in node.names]
|
||||
raise SyntaxError(
|
||||
f"Multiple imports from vmlinux are not supported. "
|
||||
f"Found: {', '.join(imported_names)}. "
|
||||
f"Please use separate import statements for each type."
|
||||
)
|
||||
|
||||
# Check if no specific import is specified (should not happen with valid Python)
|
||||
if len(node.names) == 0:
|
||||
raise SyntaxError(
|
||||
"Import from vmlinux must specify at least one type."
|
||||
)
|
||||
|
||||
# Valid single import
|
||||
for alias in node.names:
|
||||
import_name = alias.name
|
||||
# Use alias if provided, otherwise use the original name (commented)
|
||||
# as_name = alias.asname if alias.asname else alias.name
|
||||
vmlinux_imports.append(("vmlinux", node))
|
||||
logger.info(f"Found vmlinux import: {import_name}")
|
||||
|
||||
# Handle "import vmlinux" statements (not typical but should be rejected)
|
||||
elif isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
if alias.name == "vmlinux" or alias.name.startswith("vmlinux."):
|
||||
raise SyntaxError(
|
||||
"Direct import of vmlinux module is not supported. "
|
||||
"Use 'from vmlinux import <type>' instead."
|
||||
)
|
||||
|
||||
logger.info(f"Total vmlinux imports detected: {len(vmlinux_imports)}")
|
||||
return vmlinux_imports
|
||||
|
||||
|
||||
def vmlinux_proc(tree: ast.AST, module):
|
||||
import_statements = detect_import_statement(tree)
|
||||
|
||||
# initialise dependency handler
|
||||
handler = DependencyHandler()
|
||||
# initialise assignment dictionary of name to type
|
||||
assignments: dict[str, tuple[type, Any]] = {}
|
||||
|
||||
if not import_statements:
|
||||
logger.info("No vmlinux imports found")
|
||||
return
|
||||
|
||||
# Import vmlinux module directly
|
||||
try:
|
||||
vmlinux_mod = importlib.import_module("vmlinux")
|
||||
except ImportError:
|
||||
logger.warning("Could not import vmlinux module")
|
||||
return
|
||||
|
||||
source_file = inspect.getsourcefile(vmlinux_mod)
|
||||
if source_file is None:
|
||||
logger.warning("Cannot find source for vmlinux module")
|
||||
return
|
||||
|
||||
with open(source_file, "r") as f:
|
||||
mod_ast = ast.parse(f.read(), filename=source_file)
|
||||
|
||||
for import_mod, import_node in import_statements:
|
||||
for alias in import_node.names:
|
||||
imported_name = alias.name
|
||||
found = False
|
||||
for mod_node in mod_ast.body:
|
||||
if (
|
||||
isinstance(mod_node, ast.ClassDef)
|
||||
and mod_node.name == imported_name
|
||||
):
|
||||
process_vmlinux_class(mod_node, module, handler)
|
||||
found = True
|
||||
break
|
||||
if isinstance(mod_node, ast.Assign):
|
||||
for target in mod_node.targets:
|
||||
if isinstance(target, ast.Name) and target.id == imported_name:
|
||||
process_vmlinux_assign(mod_node, module, assignments)
|
||||
found = True
|
||||
break
|
||||
if found:
|
||||
break
|
||||
if not found:
|
||||
logger.info(
|
||||
f"{imported_name} not found as ClassDef or Assign in vmlinux"
|
||||
)
|
||||
|
||||
IRGenerator(module, handler)
|
||||
return assignments
|
||||
|
||||
|
||||
def process_vmlinux_assign(node, module, assignments: dict[str, tuple[type, Any]]):
|
||||
# Check if this is a simple assignment with a constant value
|
||||
if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
|
||||
target_name = node.targets[0].id
|
||||
if isinstance(node.value, ast.Constant):
|
||||
assignments[target_name] = (type(node.value.value), node.value.value)
|
||||
logger.info(
|
||||
f"Added assignment: {target_name} = {node.value.value!r} of type {type(node.value.value)}"
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unsupported assignment type for {target_name}")
|
||||
else:
|
||||
raise ValueError("Not a simple assignment")
|
||||
3
pythonbpf/vmlinux_parser/ir_gen/__init__.py
Normal file
3
pythonbpf/vmlinux_parser/ir_gen/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .ir_generation import IRGenerator
|
||||
|
||||
__all__ = ["IRGenerator"]
|
||||
15
pythonbpf/vmlinux_parser/ir_gen/debug_info_gen.py
Normal file
15
pythonbpf/vmlinux_parser/ir_gen/debug_info_gen.py
Normal file
@ -0,0 +1,15 @@
|
||||
from pythonbpf.debuginfo import DebugInfoGenerator
|
||||
|
||||
|
||||
def debug_info_generation(struct, llvm_module):
|
||||
generator = DebugInfoGenerator(llvm_module)
|
||||
# this is sample debug info generation
|
||||
# i64type = generator.get_uint64_type()
|
||||
|
||||
struct_type = generator.create_struct_type([], 64 * 4, is_distinct=True)
|
||||
|
||||
global_var = generator.create_global_var_debug_info(
|
||||
struct.name, struct_type, is_local=False
|
||||
)
|
||||
|
||||
return global_var
|
||||
161
pythonbpf/vmlinux_parser/ir_gen/ir_generation.py
Normal file
161
pythonbpf/vmlinux_parser/ir_gen/ir_generation.py
Normal file
@ -0,0 +1,161 @@
|
||||
import ctypes
|
||||
import logging
|
||||
from ..dependency_handler import DependencyHandler
|
||||
from .debug_info_gen import debug_info_generation
|
||||
from ..dependency_node import DependencyNode
|
||||
import llvmlite.ir as ir
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IRGenerator:
|
||||
# get the assignments dict and add this stuff to it.
|
||||
def __init__(self, llvm_module, handler: DependencyHandler, assignment=None):
|
||||
self.llvm_module = llvm_module
|
||||
self.handler: DependencyHandler = handler
|
||||
self.generated: list[str] = []
|
||||
if not handler.is_ready:
|
||||
raise ImportError(
|
||||
"Semantic analysis of vmlinux imports failed. Cannot generate IR"
|
||||
)
|
||||
for struct in handler:
|
||||
self.struct_processor(struct)
|
||||
|
||||
def struct_processor(self, struct, processing_stack=None):
|
||||
# Initialize processing stack on first call
|
||||
if processing_stack is None:
|
||||
processing_stack = set()
|
||||
|
||||
# If already generated, skip
|
||||
if struct.name in self.generated:
|
||||
return
|
||||
|
||||
# Detect circular dependency
|
||||
if struct.name in processing_stack:
|
||||
logger.info(
|
||||
f"Circular dependency detected for {struct.name}, skipping recursive processing"
|
||||
)
|
||||
# For circular dependencies, we can either:
|
||||
# 1. Use forward declarations (opaque pointers)
|
||||
# 2. Mark as incomplete and process later
|
||||
# 3. Generate a placeholder type
|
||||
# Here we'll just skip and let it be processed in its own call
|
||||
return
|
||||
|
||||
logger.info(f"IR generating for {struct.name}")
|
||||
|
||||
# Add to processing stack before processing dependencies
|
||||
processing_stack.add(struct.name)
|
||||
|
||||
try:
|
||||
# Process all dependencies first
|
||||
if struct.depends_on is None:
|
||||
pass
|
||||
else:
|
||||
for dependency in struct.depends_on:
|
||||
if dependency not in self.generated:
|
||||
# Check if dependency exists in handler
|
||||
if dependency in self.handler.nodes:
|
||||
dep_node_from_dependency = self.handler[dependency]
|
||||
# Pass the processing_stack down to track circular refs
|
||||
self.struct_processor(
|
||||
dep_node_from_dependency, processing_stack
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"Warning: Dependency {dependency} not found in handler"
|
||||
)
|
||||
|
||||
# Actual processor logic here after dependencies are resolved
|
||||
self.gen_ir(struct)
|
||||
self.generated.append(struct.name)
|
||||
|
||||
finally:
|
||||
# Remove from processing stack after we're done
|
||||
processing_stack.discard(struct.name)
|
||||
|
||||
def gen_ir(self, struct):
|
||||
# TODO: we add the btf_ama attribute by monkey patching in the end of compilation, but once llvmlite
|
||||
# accepts our issue, we will resort to normal accessed attribute based attribute addition
|
||||
# currently we generate all possible field accesses for CO-RE and put into the assignment table
|
||||
debug_info = debug_info_generation(struct, self.llvm_module)
|
||||
field_index = 0
|
||||
for field_name, field in struct.fields.items():
|
||||
# does not take arrays and similar types into consideration yet.
|
||||
if field.ctype_complex_type is not None and issubclass(
|
||||
field.ctype_complex_type, ctypes.Array
|
||||
):
|
||||
array_size = field.type_size
|
||||
containing_type = field.containing_type
|
||||
if containing_type.__module__ == ctypes.__name__:
|
||||
containing_type_size = ctypes.sizeof(containing_type)
|
||||
for i in range(0, array_size):
|
||||
field_co_re_name = self._struct_name_generator(
|
||||
struct, field, field_index, True, i, containing_type_size
|
||||
)
|
||||
globvar = ir.GlobalVariable(
|
||||
self.llvm_module, ir.IntType(64), name=field_co_re_name
|
||||
)
|
||||
globvar.linkage = "external"
|
||||
globvar.set_metadata("llvm.preserve.access.index", debug_info)
|
||||
field_index += 1
|
||||
elif field.type_size is not None:
|
||||
array_size = field.type_size
|
||||
containing_type = field.containing_type
|
||||
if containing_type.__module__ == "vmlinux":
|
||||
containing_type_size = self.handler[
|
||||
containing_type.__name__
|
||||
].current_offset
|
||||
for i in range(0, array_size):
|
||||
field_co_re_name = self._struct_name_generator(
|
||||
struct, field, field_index, True, i, containing_type_size
|
||||
)
|
||||
globvar = ir.GlobalVariable(
|
||||
self.llvm_module, ir.IntType(64), name=field_co_re_name
|
||||
)
|
||||
globvar.linkage = "external"
|
||||
globvar.set_metadata("llvm.preserve.access.index", debug_info)
|
||||
field_index += 1
|
||||
else:
|
||||
field_co_re_name = self._struct_name_generator(
|
||||
struct, field, field_index
|
||||
)
|
||||
field_index += 1
|
||||
globvar = ir.GlobalVariable(
|
||||
self.llvm_module, ir.IntType(64), name=field_co_re_name
|
||||
)
|
||||
globvar.linkage = "external"
|
||||
globvar.set_metadata("llvm.preserve.access.index", debug_info)
|
||||
|
||||
def _struct_name_generator(
|
||||
self,
|
||||
struct: DependencyNode,
|
||||
field,
|
||||
field_index: int,
|
||||
is_indexed: bool = False,
|
||||
index: int = 0,
|
||||
containing_type_size: int = 0,
|
||||
) -> str:
|
||||
if is_indexed:
|
||||
name = (
|
||||
"llvm."
|
||||
+ struct.name.removeprefix("struct_")
|
||||
+ f":0:{field.offset + index * containing_type_size}"
|
||||
+ "$"
|
||||
+ f"0:{field_index}:{index}"
|
||||
)
|
||||
return name
|
||||
elif struct.name.startswith("struct_"):
|
||||
name = (
|
||||
"llvm."
|
||||
+ struct.name.removeprefix("struct_")
|
||||
+ f":0:{field.offset}"
|
||||
+ "$"
|
||||
+ f"0:{field_index}"
|
||||
)
|
||||
return name
|
||||
else:
|
||||
print(self.handler[struct.name])
|
||||
raise TypeError(
|
||||
"Name generation cannot occur due to type name not starting with struct"
|
||||
)
|
||||
@ -1,11 +1,10 @@
|
||||
#include <linux/bpf.h>
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#define u64 unsigned long long
|
||||
#define u32 unsigned int
|
||||
#include <bpf/bpf_endian.h>
|
||||
|
||||
SEC("xdp")
|
||||
int hello(struct xdp_md *ctx) {
|
||||
bpf_printk("Hello, World!\n");
|
||||
bpf_printk("Hello, World! %ud \n", ctx->data);
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
@ -23,20 +23,20 @@ SEC("tracepoint/syscalls/sys_enter_clone")
|
||||
int hello(struct pt_regs *ctx)
|
||||
{
|
||||
struct data_t data = {};
|
||||
|
||||
|
||||
// Get PID (lower 32 bits of the 64-bit value returned)
|
||||
data.pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF;
|
||||
|
||||
|
||||
// Get timestamp
|
||||
data.ts = bpf_ktime_get_ns();
|
||||
|
||||
|
||||
// Get current process name
|
||||
// bpf_get_current_comm(&data.comm, sizeof(data.comm));
|
||||
|
||||
|
||||
// Submit data to userspace via perf event
|
||||
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
|
||||
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
|
||||
&data, sizeof(data));
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -1,23 +1,9 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
#include <linux/bpf.h>
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_tracing.h>
|
||||
|
||||
struct trace_entry {
|
||||
short unsigned int type;
|
||||
unsigned char flags;
|
||||
unsigned char preempt_count;
|
||||
int pid;
|
||||
};
|
||||
|
||||
struct trace_event_raw_sys_enter {
|
||||
struct trace_entry ent;
|
||||
long int id;
|
||||
long unsigned int args[6];
|
||||
char __data[0];
|
||||
};
|
||||
|
||||
struct event {
|
||||
__u32 pid;
|
||||
__u32 uid;
|
||||
27
tests/c-form/globals.bpf.c
Normal file
27
tests/c-form/globals.bpf.c
Normal file
@ -0,0 +1,27 @@
|
||||
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
|
||||
#include <linux/bpf.h>
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_tracing.h>
|
||||
#include <linux/types.h>
|
||||
|
||||
struct test_struct {
|
||||
__u64 a;
|
||||
__u64 b;
|
||||
};
|
||||
|
||||
struct test_struct w = {};
|
||||
volatile __u64 prev_time = 0;
|
||||
|
||||
SEC("tracepoint/syscalls/sys_enter_execve")
|
||||
int trace_execve(void *ctx)
|
||||
{
|
||||
bpf_printk("previous %ul now %ul", w.b, w.a);
|
||||
__u64 ts = bpf_ktime_get_ns();
|
||||
bpf_printk("prev %ul now %ul", prev_time, ts);
|
||||
w.a = ts;
|
||||
w.b = prev_time;
|
||||
prev_time = ts;
|
||||
return 0;
|
||||
}
|
||||
|
||||
char LICENSE[] SEC("license") = "GPL";
|
||||
19
tests/c-form/kprobe.bpf.c
Normal file
19
tests/c-form/kprobe.bpf.c
Normal file
@ -0,0 +1,19 @@
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_tracing.h>
|
||||
|
||||
char LICENSE[] SEC("license") = "Dual BSD/GPL";
|
||||
|
||||
SEC("kprobe/do_unlinkat")
|
||||
int kprobe_execve(struct pt_regs *ctx)
|
||||
{
|
||||
bpf_printk("unlinkat created");
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("kretprobe/do_unlinkat")
|
||||
int kretprobe_execve(struct pt_regs *ctx)
|
||||
{
|
||||
bpf_printk("unlinkat returned\n");
|
||||
return 0;
|
||||
}
|
||||
51
tests/c-form/ringbuf.bpf.c
Normal file
51
tests/c-form/ringbuf.bpf.c
Normal file
@ -0,0 +1,51 @@
|
||||
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
|
||||
#include <linux/bpf.h>
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_tracing.h>
|
||||
#include <linux/types.h>
|
||||
|
||||
// Define the structure to be sent via ringbuf
|
||||
struct event {
|
||||
__u32 pid;
|
||||
__u32 uid;
|
||||
__u64 timestamp;
|
||||
char comm[16]; // Process name
|
||||
};
|
||||
|
||||
// Define the ringbuffer map
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_RINGBUF);
|
||||
__uint(max_entries, 256 * 1024); // 256 KB
|
||||
} events SEC(".maps");
|
||||
|
||||
// Tracepoint for execve system calls
|
||||
SEC("tracepoint/syscalls/sys_enter_execve")
|
||||
int trace_execve(void *ctx)
|
||||
{
|
||||
struct event *e;
|
||||
__u64 pid_tgid;
|
||||
__u64 uid_gid;
|
||||
|
||||
// Reserve space in the ringbuffer
|
||||
e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
|
||||
if (!e)
|
||||
return 0;
|
||||
|
||||
// Fill the struct with data
|
||||
pid_tgid = bpf_get_current_pid_tgid();
|
||||
e->pid = pid_tgid >> 32;
|
||||
|
||||
uid_gid = bpf_get_current_uid_gid();
|
||||
e->uid = uid_gid & 0xFFFFFFFF;
|
||||
|
||||
e->timestamp = bpf_ktime_get_ns();
|
||||
|
||||
bpf_get_current_comm(&e->comm, sizeof(e->comm));
|
||||
|
||||
// Submit the event to ringbuffer
|
||||
bpf_ringbuf_submit(e, 0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
char LICENSE[] SEC("license") = "GPL";
|
||||
121617
tests/c-form/vmlinux.h
vendored
Normal file
121617
tests/c-form/vmlinux.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
40
tests/failing_tests/assign/retype.py
Normal file
40
tests/failing_tests/assign/retype.py
Normal file
@ -0,0 +1,40 @@
|
||||
from pythonbpf import bpf, map, section, bpfglobal, compile
|
||||
from ctypes import c_void_p, c_int64, c_uint64
|
||||
from pythonbpf.maps import HashMap
|
||||
|
||||
|
||||
# NOTE: This example tries to reinterpret the variable `x` to a different type.
|
||||
# We do not allow this for now, as stack allocations are typed and have to be
|
||||
# done in the first basic block. Allowing re-interpretation would require
|
||||
# re-allocation of stack space (possibly in a new basic block), which is not
|
||||
# supported in eBPF yet.
|
||||
# We can allow bitcasts in cases where the width of the types is the same in
|
||||
# the future. But for now, we do not allow any re-interpretation of variables.
|
||||
|
||||
|
||||
@bpf
|
||||
@map
|
||||
def last() -> HashMap:
|
||||
return HashMap(key=c_uint64, value=c_uint64, max_entries=3)
|
||||
|
||||
|
||||
@bpf
|
||||
@section("tracepoint/syscalls/sys_enter_execve")
|
||||
def hello_world(ctx: c_void_p) -> c_int64:
|
||||
last.update(0, 1)
|
||||
x = last.lookup(0)
|
||||
x = 20
|
||||
if x == 2:
|
||||
print("Hello, World!")
|
||||
else:
|
||||
print("Goodbye, World!")
|
||||
return
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def LICENSE() -> str:
|
||||
return "GPL"
|
||||
|
||||
|
||||
compile()
|
||||
34
tests/failing_tests/conditionals/helper_cond.py
Normal file
34
tests/failing_tests/conditionals/helper_cond.py
Normal file
@ -0,0 +1,34 @@
|
||||
from pythonbpf import bpf, map, section, bpfglobal, compile
|
||||
from ctypes import c_void_p, c_int64, c_uint64
|
||||
from pythonbpf.maps import HashMap
|
||||
|
||||
# NOTE: Decided against fixing this
|
||||
# as a workaround is assigning the result of lookup to a variable
|
||||
# and then using that variable in the if statement.
|
||||
# Might fix in future.
|
||||
|
||||
|
||||
@bpf
|
||||
@map
|
||||
def last() -> HashMap:
|
||||
return HashMap(key=c_uint64, value=c_uint64, max_entries=3)
|
||||
|
||||
|
||||
@bpf
|
||||
@section("tracepoint/syscalls/sys_enter_execve")
|
||||
def hello_world(ctx: c_void_p) -> c_int64:
|
||||
last.update(0, 1)
|
||||
if last.lookup(0) > 0:
|
||||
print("Hello, World!")
|
||||
else:
|
||||
print("Goodbye, World!")
|
||||
return
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def LICENSE() -> str:
|
||||
return "GPL"
|
||||
|
||||
|
||||
compile()
|
||||
18
tests/failing_tests/conditionals/oneline.py
Normal file
18
tests/failing_tests/conditionals/oneline.py
Normal file
@ -0,0 +1,18 @@
|
||||
from pythonbpf import bpf, section, bpfglobal, compile
|
||||
from ctypes import c_void_p, c_int64
|
||||
|
||||
|
||||
@bpf
|
||||
@section("tracepoint/syscalls/sys_enter_execve")
|
||||
def hello_world(ctx: c_void_p) -> c_int64:
|
||||
print("Hello, World!") if True else print("Goodbye, World!")
|
||||
return
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def LICENSE() -> str:
|
||||
return "GPL"
|
||||
|
||||
|
||||
compile()
|
||||
34
tests/failing_tests/conditionals/struct_ptr.py
Normal file
34
tests/failing_tests/conditionals/struct_ptr.py
Normal file
@ -0,0 +1,34 @@
|
||||
from pythonbpf import bpf, struct, section, bpfglobal, compile
|
||||
from ctypes import c_void_p, c_int64, c_uint64
|
||||
|
||||
# NOTE: Decided against fixing this
|
||||
# as one workaround is to just check any field of the struct
|
||||
# in the if statement. Ugly but works.
|
||||
# Might fix in future.
|
||||
|
||||
|
||||
@bpf
|
||||
@struct
|
||||
class data_t:
|
||||
pid: c_uint64
|
||||
ts: c_uint64
|
||||
|
||||
|
||||
@bpf
|
||||
@section("tracepoint/syscalls/sys_enter_execve")
|
||||
def hello_world(ctx: c_void_p) -> c_int64:
|
||||
dat = data_t()
|
||||
if dat:
|
||||
print("Hello, World!")
|
||||
else:
|
||||
print("Goodbye, World!")
|
||||
return
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def LICENSE() -> str:
|
||||
return "GPL"
|
||||
|
||||
|
||||
compile()
|
||||
45
tests/failing_tests/direct_assign.py
Normal file
45
tests/failing_tests/direct_assign.py
Normal file
@ -0,0 +1,45 @@
|
||||
from pythonbpf import bpf, map, section, bpfglobal, compile
|
||||
from pythonbpf.helper import XDP_PASS
|
||||
from pythonbpf.maps import HashMap
|
||||
|
||||
from ctypes import c_void_p, c_int64
|
||||
|
||||
# NOTE: I have decided to not fix this example for now.
|
||||
# The issue is in line 31, where we are passing an expression.
|
||||
# The update helper expects a pointer type. But the problem is
|
||||
# that we must allocate the space for said pointer in the first
|
||||
# basic block. As that usage is in a different basic block, we
|
||||
# are unable to cast the expression to a pointer type. (as we never
|
||||
# allocated space for it).
|
||||
# Shall we change our space allocation logic? That allows users to
|
||||
# spam the same helper with the same args, and still run out of
|
||||
# stack space. So we consider this usage invalid for now.
|
||||
# Might fix it later.
|
||||
|
||||
|
||||
@bpf
|
||||
@map
|
||||
def count() -> HashMap:
|
||||
return HashMap(key=c_int64, value=c_int64, max_entries=1)
|
||||
|
||||
|
||||
@bpf
|
||||
@section("xdp")
|
||||
def hello_world(ctx: c_void_p) -> c_int64:
|
||||
prev = count.lookup(0)
|
||||
if prev:
|
||||
count.update(0, prev + 1)
|
||||
return XDP_PASS
|
||||
else:
|
||||
count.update(0, 1)
|
||||
|
||||
return XDP_PASS
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def LICENSE() -> str:
|
||||
return "GPL"
|
||||
|
||||
|
||||
compile()
|
||||
109
tests/failing_tests/globals.py
Normal file
109
tests/failing_tests/globals.py
Normal file
@ -0,0 +1,109 @@
|
||||
import logging
|
||||
|
||||
from pythonbpf import compile, bpf, section, bpfglobal, compile_to_ir
|
||||
from ctypes import c_void_p, c_int64, c_int32
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def somevalue() -> c_int32:
|
||||
return c_int32(42)
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def somevalue2() -> c_int64:
|
||||
return c_int64(69)
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def somevalue1() -> c_int32:
|
||||
return c_int32(42)
|
||||
|
||||
|
||||
# --- Passing examples ---
|
||||
|
||||
|
||||
# Simple constant return
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def g1() -> c_int64:
|
||||
return c_int64(42)
|
||||
|
||||
|
||||
# Constructor with one constant argument
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def g2() -> c_int64:
|
||||
return c_int64(69)
|
||||
|
||||
|
||||
# --- Failing examples ---
|
||||
|
||||
# No return annotation
|
||||
# @bpf
|
||||
# @bpfglobal
|
||||
# def g3():
|
||||
# return 42
|
||||
|
||||
# Return annotation is complex
|
||||
# @bpf
|
||||
# @bpfglobal
|
||||
# def g4() -> List[int]:
|
||||
# return []
|
||||
|
||||
# # Return is missing
|
||||
# @bpf
|
||||
# @bpfglobal
|
||||
# def g5() -> c_int64:
|
||||
# pass
|
||||
|
||||
# # Return is a variable reference
|
||||
# #TODO: maybe fix this sometime later. It defaults to 0
|
||||
# CONST = 5
|
||||
# @bpf
|
||||
# @bpfglobal
|
||||
# def g6() -> c_int64:
|
||||
# return c_int64(CONST)
|
||||
|
||||
|
||||
# Constructor with multiple args
|
||||
# TODO: this is not working. should it work ?
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def g7() -> c_int64:
|
||||
return c_int64(1)
|
||||
|
||||
|
||||
# Dataclass call
|
||||
# TODO: fails with dataclass
|
||||
# @dataclass
|
||||
# class Point:
|
||||
# x: c_int64
|
||||
# y: c_int64
|
||||
|
||||
# @bpf
|
||||
# @bpfglobal
|
||||
# def g8() -> Point:
|
||||
# return Point(1, 2)
|
||||
|
||||
|
||||
@bpf
|
||||
@section("tracepoint/syscalls/sys_enter_execve")
|
||||
def sometag(ctx: c_void_p) -> c_int64:
|
||||
print("test")
|
||||
global somevalue
|
||||
somevalue = 2
|
||||
print(f"{somevalue}")
|
||||
return c_int64(1)
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def LICENSE() -> str:
|
||||
return "GPL"
|
||||
|
||||
|
||||
compile_to_ir("globals.py", "globals.ll", loglevel=logging.INFO)
|
||||
compile()
|
||||
19
tests/failing_tests/if.py
Normal file
19
tests/failing_tests/if.py
Normal file
@ -0,0 +1,19 @@
|
||||
from pythonbpf import compile, bpf, section, bpfglobal
|
||||
from ctypes import c_void_p, c_int64
|
||||
|
||||
|
||||
@bpf
|
||||
@section("sometag1")
|
||||
def sometag(ctx: c_void_p) -> c_int64:
|
||||
if 3 + 2 == 5:
|
||||
return c_int64(5)
|
||||
return c_int64(0)
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def LICENSE() -> str:
|
||||
return "GPL"
|
||||
|
||||
|
||||
compile()
|
||||
14
tests/failing_tests/license.py
Normal file
14
tests/failing_tests/license.py
Normal file
@ -0,0 +1,14 @@
|
||||
from pythonbpf import compile, bpf, section
|
||||
from ctypes import c_void_p, c_int64
|
||||
|
||||
|
||||
# FAILS WHEN THERE IS NO LICENSE. which is wrong.
|
||||
@bpf
|
||||
@section("sometag1")
|
||||
def sometag(ctx: c_void_p) -> c_int64:
|
||||
a = 1 + 2
|
||||
print(f"{a}")
|
||||
return c_int64(0)
|
||||
|
||||
|
||||
compile()
|
||||
41
tests/failing_tests/named_arg.py
Normal file
41
tests/failing_tests/named_arg.py
Normal file
@ -0,0 +1,41 @@
|
||||
from pythonbpf import bpf, map, section, bpfglobal, compile
|
||||
from pythonbpf.helper import XDP_PASS
|
||||
from pythonbpf.maps import HashMap
|
||||
|
||||
from ctypes import c_void_p, c_int64
|
||||
|
||||
# NOTE: This example exposes the problems with our typing system.
|
||||
# We can't do steps on line 25 and 27.
|
||||
# prev is of type i64**. For prev + 1, we deref it down to i64
|
||||
# To assign it back to prev, we need to go back to i64**.
|
||||
# We cannot allocate space for the intermediate type now.
|
||||
# We probably need to track the ref/deref chain for each variable.
|
||||
|
||||
|
||||
@bpf
|
||||
@map
|
||||
def count() -> HashMap:
|
||||
return HashMap(key=c_int64, value=c_int64, max_entries=1)
|
||||
|
||||
|
||||
@bpf
|
||||
@section("xdp")
|
||||
def hello_world(ctx: c_void_p) -> c_int64:
|
||||
prev = count.lookup(0)
|
||||
if prev:
|
||||
prev = prev + 1
|
||||
count.update(0, prev)
|
||||
return XDP_PASS
|
||||
else:
|
||||
count.update(0, 1)
|
||||
|
||||
return XDP_PASS
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def LICENSE() -> str:
|
||||
return "GPL"
|
||||
|
||||
|
||||
compile()
|
||||
23
tests/failing_tests/undeclared_values.py
Normal file
23
tests/failing_tests/undeclared_values.py
Normal file
@ -0,0 +1,23 @@
|
||||
import logging
|
||||
|
||||
from pythonbpf import compile, bpf, section, bpfglobal, compile_to_ir
|
||||
from ctypes import c_void_p, c_int64
|
||||
|
||||
|
||||
# This should not pass as somevalue is not declared at all.
|
||||
@bpf
|
||||
@section("tracepoint/syscalls/sys_enter_execve")
|
||||
def sometag(ctx: c_void_p) -> c_int64:
|
||||
print("test")
|
||||
print(f"{somevalue}") # noqa: F821
|
||||
return c_int64(1)
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def LICENSE() -> str:
|
||||
return "GPL"
|
||||
|
||||
|
||||
compile_to_ir("globals.py", "globals.ll", loglevel=logging.INFO)
|
||||
compile()
|
||||
54
tests/failing_tests/xdp_pass.py
Normal file
54
tests/failing_tests/xdp_pass.py
Normal file
@ -0,0 +1,54 @@
|
||||
from pythonbpf import bpf, map, section, bpfglobal, compile_to_ir
|
||||
from pythonbpf.maps import HashMap
|
||||
from pythonbpf.helper import XDP_PASS
|
||||
from vmlinux import TASK_COMM_LEN # noqa: F401
|
||||
|
||||
from vmlinux import struct_qspinlock # noqa: F401
|
||||
|
||||
# from vmlinux import struct_trace_event_raw_sys_enter # noqa: F401
|
||||
# from vmlinux import struct_posix_cputimers # noqa: F401
|
||||
from vmlinux import struct_xdp_md
|
||||
|
||||
# from vmlinux import struct_trace_event_raw_sys_enter # noqa: F401
|
||||
# from vmlinux import struct_ring_buffer_per_cpu # noqa: F401
|
||||
# from vmlinux import struct_request # noqa: F401
|
||||
from ctypes import c_int64
|
||||
|
||||
# Instructions to how to run this program
|
||||
# 1. Install PythonBPF: pip install pythonbpf
|
||||
# 2. Run the program: python examples/xdp_pass.py
|
||||
# 3. Run the program with sudo: sudo tools/check.sh run examples/xdp_pass.o
|
||||
# 4. Attach object file to any network device with something like ./check.sh xdp examples/xdp_pass.o tailscale0
|
||||
# 5. send traffic through the device and observe effects
|
||||
|
||||
|
||||
@bpf
|
||||
@map
|
||||
def count() -> HashMap:
|
||||
return HashMap(key=c_int64, value=c_int64, max_entries=1)
|
||||
|
||||
|
||||
@bpf
|
||||
@section("xdp")
|
||||
def hello_world(ctx: struct_xdp_md) -> c_int64:
|
||||
key = 0
|
||||
one = 1
|
||||
prev = count().lookup(key)
|
||||
if prev:
|
||||
prevval = prev + 1
|
||||
print(f"count: {prevval}")
|
||||
count().update(key, prevval)
|
||||
return XDP_PASS
|
||||
else:
|
||||
count().update(key, one)
|
||||
|
||||
return XDP_PASS
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def LICENSE() -> str:
|
||||
return "GPL"
|
||||
|
||||
|
||||
compile_to_ir("xdp_pass.py", "xdp_pass.ll")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user