mirror of
https://github.com/varun-r-mallya/Python-BPF.git
synced 2026-02-08 06:00:56 +00:00
Compare commits
730 Commits
26f8f769c5
...
copilot/cr
| Author | SHA1 | Date | |
|---|---|---|---|
| b6ecec9889 | |||
| 4f56f8c426 | |||
| 3bff930e98 | |||
| 036830c200 | |||
| aded125cba | |||
| 581269e52b | |||
| 8bfd998863 | |||
| a31ef3997a | |||
| 217e760e98 | |||
| 06c81ae55e | |||
| 9131d044dc | |||
| 2840a5c101 | |||
| 9ff33229a0 | |||
| 2e95b77ceb | |||
| c6aa1077de | |||
| 220adaf011 | |||
| 92162e5cb4 | |||
| e0251a05bf | |||
| f03c08703a | |||
| 4edfb18609 | |||
| c58483ab81 | |||
| 2d8c6c144c | |||
| c1f32a2839 | |||
| b6d8b71308 | |||
| ab881772af | |||
| b03924836e | |||
| 4ab1e26b92 | |||
| 73f6e83445 | |||
| c1e90b9d46 | |||
| 917d386d33 | |||
| ef128f3752 | |||
| b92208ed0d | |||
| 2bd8e73724 | |||
| 641f8bacbe | |||
| 749b06020d | |||
| 0ce5add39b | |||
| d0e2360f46 | |||
| 049ec55e85 | |||
| 77901accf2 | |||
| 0616a2fccb | |||
| 526425a267 | |||
| 466ecdb6a4 | |||
| 752a10fa5f | |||
| 3602b502f4 | |||
| 808db2722d | |||
| 99fc5d75cc | |||
| c91e69e2f7 | |||
| dc995a1448 | |||
| 0fd6bea211 | |||
| 01d234ac86 | |||
| c97efb2570 | |||
| 76c982e15e | |||
| 2543826e85 | |||
| 650744f843 | |||
| d73c793989 | |||
| bbe4990878 | |||
| 600993f626 | |||
| 6c55d56ef0 | |||
| 704b0d8cd3 | |||
| 0e50079d88 | |||
| d457f87410 | |||
| 4ea02745b3 | |||
| 84edddb685 | |||
| 6f017a9176 | |||
| 24e5829b80 | |||
| 2daedc5882 | |||
| 14af7ec4dd | |||
| 536ea4855e | |||
| 5ba29db362 | |||
| 0ca835079d | |||
| de8c486461 | |||
| f135cdbcc0 | |||
| a8595ff1d2 | |||
| d43d3ad637 | |||
| 9becee8f77 | |||
| 189526d5ca | |||
| 1593b7bcfe | |||
| 127852ee9f | |||
| 4905649700 | |||
| 7b7b00dbe7 | |||
| 102e4ca78c | |||
| 2fd4fefbcc | |||
| 016fd5de5c | |||
| 8ad5fb8a3a | |||
| bf9635e324 | |||
| cbe365d760 | |||
| fed6af1ed6 | |||
| 18886816fb | |||
| a2de15fb1e | |||
| 9def969592 | |||
| 081ee5cb4c | |||
| a91c3158ad | |||
| 2b3635fe20 | |||
| 6f25c554a9 | |||
| 84507b8b98 | |||
| a42a75179d | |||
| 377fa4041d | |||
| 99321c7669 | |||
| 11850d16d3 | |||
| 9ee821c7f6 | |||
| 25394059a6 | |||
| fde8eab775 | |||
| 42b8865a56 | |||
| 144d9b0ab4 | |||
| 902a52a07d | |||
| 306570953b | |||
| 740eed45e1 | |||
| c8801f4c3e | |||
| e5b3b001ce | |||
| 19b42b9a19 | |||
| 9f5ec62383 | |||
| 7af54df7c0 | |||
| 573bbb350e | |||
| 64679f8072 | |||
| 5667facf23 | |||
| 4f8af16a17 | |||
| b84884162d | |||
| e9bb90cb70 | |||
| 49740598ea | |||
| 73bbf00e7c | |||
| 9d76502d5a | |||
| a10da4a277 | |||
| 29e90601b7 | |||
| 56df05a93c | |||
| a55efc6469 | |||
| 64cd2d2fc2 | |||
| cbddc0aa96 | |||
| 209df33c8f | |||
| 7a56e5d0cd | |||
| 1d7a436c9f | |||
| 5eaeb3e921 | |||
| cd52d0d91b | |||
| df981be095 | |||
| 316c21c428 | |||
| c883d95655 | |||
| f7dee329cb | |||
| 5031f90377 | |||
| 95a624044a | |||
| c5bef26b88 | |||
| 5a8b64f1d9 | |||
| cf99b3bb9a | |||
| 6c85b248ce | |||
| b5a3494cc6 | |||
| be62972974 | |||
| 2f4a7d2f90 | |||
| 3ccd3f767e | |||
| 2e37726922 | |||
| 5b36726b7d | |||
| faad3555dc | |||
| 3e6cea2b67 | |||
| 5ad33b011e | |||
| 2f4785b796 | |||
| c5fdd3bce2 | |||
| b0d35693b9 | |||
| 44c6ceda27 | |||
| 2685d0a0ee | |||
| 338d4994d8 | |||
| 3078d4224d | |||
| 7d29790f00 | |||
| 963e2a8171 | |||
| 123a92af1d | |||
| 752f564d3f | |||
| d8cddb9799 | |||
| 33e18f6d6d | |||
| 5e371787eb | |||
| 67c9d9b932 | |||
| f757a32a63 | |||
| c5de92b9d0 | |||
| 4efd3223cd | |||
| 4884ed7577 | |||
| 5b7769dd38 | |||
| b7c1e92f05 | |||
| 8b28a927c3 | |||
| 3489f45b63 | |||
| 204ec26154 | |||
| f9ee43e7ef | |||
| dabb8bf0df | |||
| 19dedede53 | |||
| 82cac8f8ef | |||
| 70a04f54d1 | |||
| ec2ea835e5 | |||
| 2257c175ed | |||
| 5bf60d69b8 | |||
| a9d82d40d3 | |||
| 85a62d6cd8 | |||
| c3fc790c71 | |||
| 22e30f04b4 | |||
| 620b8cb1e7 | |||
| 1207fe9f92 | |||
| b138405931 | |||
| 262f00f635 | |||
| 07580dabf2 | |||
| ac74b03b14 | |||
| 3bf85e733e | |||
| 73f7c80eca | |||
| 238697469a | |||
| 0006e26b08 | |||
| 5cbd9a531e | |||
| 8bd210cede | |||
| 7bf6f9c48c | |||
| a1fe2ed4bc | |||
| 93285dbdd8 | |||
| 1ea44dd8e1 | |||
| 96216d4411 | |||
| 028d9c2c08 | |||
| c6b5ecb47e | |||
| 30bcfcbbd0 | |||
| f18a4399ea | |||
| 4e01df735f | |||
| 64674cf646 | |||
| 5c1e7103a6 | |||
| 576fa2f106 | |||
| 76a873cb0d | |||
| e86c6082c9 | |||
| cb1ad15f43 | |||
| b24b3ed250 | |||
| beaad996db | |||
| 99b92e44e3 | |||
| ce7adaadb6 | |||
| 5ac316a1ac | |||
| 36a1a0903e | |||
| f2bc7f1434 | |||
| b3921c424d | |||
| 7a99d21b24 | |||
| cf05e4959d | |||
| a7394ccafa | |||
| 63f378c34b | |||
| 37af7d2e20 | |||
| 77c0d131be | |||
| 84fdf52658 | |||
| f4d903d4b5 | |||
| f9494c870b | |||
| 0d4ebf72b6 | |||
| adf32560a0 | |||
| c65900b733 | |||
| 711e34cae1 | |||
| cf3f4a0002 | |||
| d50157fa09 | |||
| ba860b5039 | |||
| 21cea97d78 | |||
| d8729342dc | |||
| 4179fbfc88 | |||
| ba397036b4 | |||
| 798f07986a | |||
| caecb8c9b0 | |||
| 1a0e21eaa8 | |||
| e98d5684ea | |||
| 190baf2674 | |||
| c07707a9ad | |||
| c3f3d1e564 | |||
| e7734629a5 | |||
| 5955db88cf | |||
| 66caa3cf1d | |||
| e499c29d42 | |||
| 76d0dbfbf4 | |||
| 56a2fbaf5b | |||
| 3b323132f0 | |||
| c9363e62a9 | |||
| a20643f3a7 | |||
| d0fecbc03c | |||
| 174095973b | |||
| 3273620447 | |||
| 610cbe82a8 | |||
| 54c97e648b | |||
| dd9411b7b9 | |||
| aa85d0e0ef | |||
| eee212795f | |||
| 8da50b7068 | |||
| e636fcaea7 | |||
| 5bba8dce12 | |||
| 8c976e46ae | |||
| 5512bf52e4 | |||
| 079ceaa0d6 | |||
| 328b792e4e | |||
| 5dafa5bd0d | |||
| 33aa794718 | |||
| d855e9ef2e | |||
| de19c8fc90 | |||
| dc1b243e82 | |||
| 1b4272b408 | |||
| 101183c315 | |||
| 3a3116253f | |||
| 9b7aa6d8be | |||
| 60737d9894 | |||
| fc55b7ecaa | |||
| c143739a04 | |||
| 51a1be0b0b | |||
| 7ae629e8f7 | |||
| dd734ea2aa | |||
| 71d005b6b1 | |||
| 5d9a29ee8e | |||
| 041e538b53 | |||
| 5413cc793b | |||
| f21837aefe | |||
| 0f5c1fa752 | |||
| de02731ea1 | |||
| c22d85ceb8 | |||
| 009b11aca6 | |||
| 2b3c81affa | |||
| 8372111616 | |||
| 9fc3c85b75 | |||
| eb4ee64ee5 | |||
| ce7b170fea | |||
| fd630293f7 | |||
| 81f72a7698 | |||
| 9a60dd87e3 | |||
| c499fe7421 | |||
| 8239097fbb | |||
| a4cfc2b7aa | |||
| fb480639a5 | |||
| 13a804f7ac | |||
| a0d954b20b | |||
| b105c70b38 | |||
| 69b73003ca | |||
| 11e8e72188 | |||
| 0a1557e318 | |||
| c56928bc8a | |||
| dd3fc74d09 | |||
| 4a79f9b9b2 | |||
| b676a5ebb4 | |||
| d7329ad3d7 | |||
| 903654daff | |||
| 263402d137 | |||
| 37d1e1b143 | |||
| edc33733d9 | |||
| d3f0e3b2ef | |||
| 09ba749b46 | |||
| a03d3e5d4c | |||
| e1f9ac6ba0 | |||
| 18d62d605a | |||
| 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 |
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
tests/c-form/vmlinux.h linguist-vendored
|
||||||
|
examples/ linguist-vendored
|
||||||
|
BCC-Examples/ 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@v6
|
||||||
|
- uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
- uses: pre-commit/action@v3.0.1
|
||||||
8
.github/workflows/python-publish.yml
vendored
8
.github/workflows/python-publish.yml
vendored
@ -20,9 +20,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ jobs:
|
|||||||
python -m build
|
python -m build
|
||||||
|
|
||||||
- name: Upload distributions
|
- name: Upload distributions
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: release-dists
|
name: release-dists
|
||||||
path: dist/
|
path: dist/
|
||||||
@ -59,7 +59,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Retrieve release distributions
|
- name: Retrieve release distributions
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: release-dists
|
name: release-dists
|
||||||
path: dist/
|
path: dist/
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@ -7,3 +7,10 @@ __pycache__/
|
|||||||
*.ll
|
*.ll
|
||||||
*.o
|
*.o
|
||||||
.ipynb_checkpoints/
|
.ipynb_checkpoints/
|
||||||
|
vmlinux.py
|
||||||
|
~*
|
||||||
|
vmlinux.h
|
||||||
|
|
||||||
|
# Documentation build artifacts
|
||||||
|
docs/_build/
|
||||||
|
docs/_templates/
|
||||||
|
|||||||
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$'
|
||||||
31
BCC-Examples/README.md
Normal file
31
BCC-Examples/README.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
## BCC examples ported to PythonBPF
|
||||||
|
|
||||||
|
This folder contains examples of BCC tutorial examples that have been ported to use **PythonBPF**.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
- install `pythonbpf` and `pylibbpf` using pip.
|
||||||
|
- You will also need `matplotlib` for vfsreadlat.py example.
|
||||||
|
- You will also need `rich` for vfsreadlat_rich.py example.
|
||||||
|
- You will also need `plotly` and `dash` for vfsreadlat_plotly.py example.
|
||||||
|
- All of these are added to `requirements.txt` file. You can install them using the following command:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
- You'll need root privileges to run these examples. If you are using a virtualenv, use the following command to run the scripts:
|
||||||
|
```bash
|
||||||
|
sudo <path_to_virtualenv>/bin/python3 <script_name>.py
|
||||||
|
```
|
||||||
|
- For the disksnoop and container-monitor examples, you need to generate the vmlinux.py file first. Follow the instructions in the [main README](https://github.com/pythonbpf/Python-BPF/tree/master?tab=readme-ov-file#first-generate-the-vmlinuxpy-file-for-your-kernel) to generate the vmlinux.py file.
|
||||||
|
- For vfsreadlat_plotly.py, run the following command to start the Dash server:
|
||||||
|
```bash
|
||||||
|
sudo <path_to_virtualenv>/bin/python3 vfsreadlat_plotly/bpf_program.py
|
||||||
|
```
|
||||||
|
Then open your web browser and navigate to the given URL.
|
||||||
|
- For container-monitor, you need to first copy the vmlinux.py to `container-monitor/` directory.
|
||||||
|
Then run the following command to run the example:
|
||||||
|
```bash
|
||||||
|
cp vmlinux.py container-monitor/
|
||||||
|
sudo <path_to_virtualenv>/bin/python3 container-monitor/container_monitor.py
|
||||||
|
```
|
||||||
122
BCC-Examples/disksnoop.ipynb
Normal file
122
BCC-Examples/disksnoop.ipynb
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "c3520e58-e50f-4bc1-8f9d-a6fecbf6e9f0",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from vmlinux import struct_request, struct_pt_regs\n",
|
||||||
|
"from pythonbpf import bpf, section, bpfglobal, map, BPF\n",
|
||||||
|
"from pythonbpf.helper import ktime\n",
|
||||||
|
"from pythonbpf.maps import HashMap\n",
|
||||||
|
"from ctypes import c_int64, c_uint64, c_int32\n",
|
||||||
|
"\n",
|
||||||
|
"REQ_WRITE = 1\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@bpf\n",
|
||||||
|
"@map\n",
|
||||||
|
"def start() -> HashMap:\n",
|
||||||
|
" return HashMap(key=c_uint64, value=c_uint64, max_entries=10240)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@bpf\n",
|
||||||
|
"@section(\"kprobe/blk_mq_end_request\")\n",
|
||||||
|
"def trace_completion(ctx: struct_pt_regs) -> c_int64:\n",
|
||||||
|
" # Get request pointer from first argument\n",
|
||||||
|
" req_ptr = ctx.di\n",
|
||||||
|
" req = struct_request(ctx.di)\n",
|
||||||
|
" # Print: data_len, cmd_flags, latency_us\n",
|
||||||
|
" data_len = req.__data_len\n",
|
||||||
|
" cmd_flags = req.cmd_flags\n",
|
||||||
|
" # Lookup start timestamp\n",
|
||||||
|
" req_tsp = start.lookup(req_ptr)\n",
|
||||||
|
" if req_tsp:\n",
|
||||||
|
" # Calculate delta in nanoseconds\n",
|
||||||
|
" delta = ktime() - req_tsp\n",
|
||||||
|
"\n",
|
||||||
|
" # Convert to microseconds for printing\n",
|
||||||
|
" delta_us = delta // 1000\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\"{data_len} {cmd_flags:x} {delta_us}\\n\")\n",
|
||||||
|
"\n",
|
||||||
|
" # Delete the entry\n",
|
||||||
|
" start.delete(req_ptr)\n",
|
||||||
|
"\n",
|
||||||
|
" return c_int64(0)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@bpf\n",
|
||||||
|
"@section(\"kprobe/blk_mq_start_request\")\n",
|
||||||
|
"def trace_start(ctx1: struct_pt_regs) -> c_int32:\n",
|
||||||
|
" req = ctx1.di\n",
|
||||||
|
" ts = ktime()\n",
|
||||||
|
" start.update(req, ts)\n",
|
||||||
|
" return c_int32(0)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@bpf\n",
|
||||||
|
"@bpfglobal\n",
|
||||||
|
"def LICENSE() -> str:\n",
|
||||||
|
" return \"GPL\"\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"b = BPF()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "97040f73-98e0-4993-94c6-125d1b42d931",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"b.load()\n",
|
||||||
|
"b.attach_all()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "b1bd4f51-fa25-42e1-877c-e48a2605189f",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from pythonbpf import trace_pipe"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "96b4b59b-b0db-4952-9534-7a714f685089",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"trace_pipe()"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3 (ipykernel)",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.12.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
48
BCC-Examples/disksnoop.py
Normal file
48
BCC-Examples/disksnoop.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
from ctypes import c_int32, c_int64, c_uint64
|
||||||
|
|
||||||
|
from vmlinux import struct_pt_regs, struct_request
|
||||||
|
|
||||||
|
from pythonbpf import bpf, bpfglobal, compile, map, section
|
||||||
|
from pythonbpf.helper import ktime
|
||||||
|
from pythonbpf.maps import HashMap
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def start() -> HashMap:
|
||||||
|
return HashMap(key=c_uint64, value=c_uint64, max_entries=10240)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("kprobe/blk_mq_end_request")
|
||||||
|
def trace_completion(ctx: struct_pt_regs) -> c_int64:
|
||||||
|
req_ptr = ctx.di
|
||||||
|
req = struct_request(ctx.di)
|
||||||
|
data_len = req.__data_len
|
||||||
|
cmd_flags = req.cmd_flags
|
||||||
|
req_tsp = start.lookup(req_ptr)
|
||||||
|
if req_tsp:
|
||||||
|
delta = ktime() - req_tsp
|
||||||
|
delta_us = delta // 1000
|
||||||
|
print(f"{data_len} {cmd_flags:x} {delta_us}\n")
|
||||||
|
start.delete(req_ptr)
|
||||||
|
|
||||||
|
return c_int64(0)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("kprobe/blk_mq_start_request")
|
||||||
|
def trace_start(ctx1: struct_pt_regs) -> c_int32:
|
||||||
|
req = ctx1.di
|
||||||
|
ts = ktime()
|
||||||
|
start.update(req, ts)
|
||||||
|
return c_int32(0)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
|
||||||
|
compile()
|
||||||
83
BCC-Examples/hello_fields.ipynb
Normal file
83
BCC-Examples/hello_fields.ipynb
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "28cf2e27-41e2-461c-a39c-147417141a4e",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from pythonbpf import bpf, section, bpfglobal, BPF, trace_fields\n",
|
||||||
|
"from ctypes import c_void_p, c_int64"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "133190e5-5a99-4585-b6e1-91224ed973c2",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"@bpf\n",
|
||||||
|
"@section(\"tracepoint/syscalls/sys_enter_clone\")\n",
|
||||||
|
"def hello_world(ctx: c_void_p) -> c_int64:\n",
|
||||||
|
" print(\"Hello, World!\")\n",
|
||||||
|
" return 0\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@bpf\n",
|
||||||
|
"@bpfglobal\n",
|
||||||
|
"def LICENSE() -> str:\n",
|
||||||
|
" return \"GPL\"\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"# Compile and load\n",
|
||||||
|
"b = BPF()\n",
|
||||||
|
"b.load()\n",
|
||||||
|
"b.attach_all()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "d3934efb-4043-4545-ae4c-c50ec40a24fd",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# header\n",
|
||||||
|
"print(f\"{'TIME(s)':<18} {'COMM':<16} {'PID':<6} {'MESSAGE'}\")\n",
|
||||||
|
"\n",
|
||||||
|
"# format output\n",
|
||||||
|
"while True:\n",
|
||||||
|
" try:\n",
|
||||||
|
" (task, pid, cpu, flags, ts, msg) = trace_fields()\n",
|
||||||
|
" except ValueError:\n",
|
||||||
|
" continue\n",
|
||||||
|
" except KeyboardInterrupt:\n",
|
||||||
|
" exit()\n",
|
||||||
|
" print(f\"{ts:<18} {task:<16} {pid:<6} {msg}\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3 (ipykernel)",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.13.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
34
BCC-Examples/hello_fields.py
Normal file
34
BCC-Examples/hello_fields.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from pythonbpf import bpf, section, bpfglobal, BPF, trace_fields
|
||||||
|
from ctypes import c_void_p, c_int64
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_clone")
|
||||||
|
def hello_world(ctx: c_void_p) -> c_int64:
|
||||||
|
print("Hello, World!")
|
||||||
|
return 0 # type: ignore [return-value]
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
|
||||||
|
# Compile and load
|
||||||
|
b = BPF()
|
||||||
|
b.load()
|
||||||
|
b.attach_all()
|
||||||
|
|
||||||
|
# header
|
||||||
|
print(f"{'TIME(s)':<18} {'COMM':<16} {'PID':<6} {'MESSAGE'}")
|
||||||
|
|
||||||
|
# format output
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
(task, pid, cpu, flags, ts, msg) = trace_fields()
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
exit()
|
||||||
|
print(f"{ts:<18} {task:<16} {pid:<6} {msg}")
|
||||||
110
BCC-Examples/hello_perf_output.ipynb
Normal file
110
BCC-Examples/hello_perf_output.ipynb
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "79b74928-f4b4-4320-96e3-d973997de2f4",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from pythonbpf import bpf, map, struct, section, bpfglobal, BPF\n",
|
||||||
|
"from pythonbpf.helper import ktime, pid, comm\n",
|
||||||
|
"from pythonbpf.maps import PerfEventArray\n",
|
||||||
|
"from ctypes import c_void_p, c_int64"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "5bdb0329-ae2d-45e8-808e-5ed5b1374204",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"@bpf\n",
|
||||||
|
"@struct\n",
|
||||||
|
"class data_t:\n",
|
||||||
|
" pid: c_int64\n",
|
||||||
|
" ts: c_int64\n",
|
||||||
|
" comm: str(16)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@bpf\n",
|
||||||
|
"@map\n",
|
||||||
|
"def events() -> PerfEventArray:\n",
|
||||||
|
" return PerfEventArray(key_size=c_int64, value_size=c_int64)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@bpf\n",
|
||||||
|
"@section(\"tracepoint/syscalls/sys_enter_clone\")\n",
|
||||||
|
"def hello(ctx: c_void_p) -> c_int64:\n",
|
||||||
|
" dataobj = data_t()\n",
|
||||||
|
" dataobj.pid, dataobj.ts = pid(), ktime()\n",
|
||||||
|
" comm(dataobj.comm)\n",
|
||||||
|
" events.output(dataobj)\n",
|
||||||
|
" return 0\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@bpf\n",
|
||||||
|
"@bpfglobal\n",
|
||||||
|
"def LICENSE() -> str:\n",
|
||||||
|
" return \"GPL\"\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"# Compile and load\n",
|
||||||
|
"b = BPF()\n",
|
||||||
|
"b.load()\n",
|
||||||
|
"b.attach_all()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "4bcc7d57-6cc4-48a3-bbd2-42ad6263afdf",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"start = 0\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def callback(cpu, event):\n",
|
||||||
|
" global start\n",
|
||||||
|
" if start == 0:\n",
|
||||||
|
" start = event.ts\n",
|
||||||
|
" ts = (event.ts - start) / 1e9\n",
|
||||||
|
" print(f\"[CPU {cpu}] PID: {event.pid}, TS: {ts}, COMM: {event.comm.decode()}\")\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"perf = b[\"events\"].open_perf_buffer(callback, struct_name=\"data_t\")\n",
|
||||||
|
"print(\"Starting to poll... (Ctrl+C to stop)\")\n",
|
||||||
|
"print(\"Try running: fork() or clone() system calls to trigger events\")\n",
|
||||||
|
"\n",
|
||||||
|
"try:\n",
|
||||||
|
" while True:\n",
|
||||||
|
" b[\"events\"].poll(1000)\n",
|
||||||
|
"except KeyboardInterrupt:\n",
|
||||||
|
" print(\"Stopping...\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3 (ipykernel)",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.13.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
61
BCC-Examples/hello_perf_output.py
Normal file
61
BCC-Examples/hello_perf_output.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
from pythonbpf import bpf, map, struct, section, bpfglobal, BPF
|
||||||
|
from pythonbpf.helper import ktime, pid, comm
|
||||||
|
from pythonbpf.maps import PerfEventArray
|
||||||
|
from ctypes import c_void_p, c_int64
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class data_t:
|
||||||
|
pid: c_int64
|
||||||
|
ts: c_int64
|
||||||
|
comm: str(16) # type: ignore [valid-type]
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def events() -> PerfEventArray:
|
||||||
|
return PerfEventArray(key_size=c_int64, value_size=c_int64)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_clone")
|
||||||
|
def hello(ctx: c_void_p) -> c_int64:
|
||||||
|
dataobj = data_t()
|
||||||
|
dataobj.pid, dataobj.ts = pid(), ktime()
|
||||||
|
comm(dataobj.comm)
|
||||||
|
events.output(dataobj)
|
||||||
|
return 0 # type: ignore [return-value]
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
|
||||||
|
# Compile and load
|
||||||
|
b = BPF()
|
||||||
|
b.load()
|
||||||
|
b.attach_all()
|
||||||
|
|
||||||
|
start = 0
|
||||||
|
|
||||||
|
|
||||||
|
def callback(cpu, event):
|
||||||
|
global start
|
||||||
|
if start == 0:
|
||||||
|
start = event.ts
|
||||||
|
ts = (event.ts - start) / 1e9
|
||||||
|
print(f"[CPU {cpu}] PID: {event.pid}, TS: {ts}, COMM: {event.comm.decode()}")
|
||||||
|
|
||||||
|
|
||||||
|
perf = b["events"].open_perf_buffer(callback, struct_name="data_t")
|
||||||
|
print("Starting to poll... (Ctrl+C to stop)")
|
||||||
|
print("Try running: fork() or clone() system calls to trigger events")
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
b["events"].poll(1000)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Stopping...")
|
||||||
116
BCC-Examples/hello_world.ipynb
Normal file
116
BCC-Examples/hello_world.ipynb
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 9,
|
||||||
|
"id": "7d5d3cfb-39ba-4516-9856-b3bed47a0cef",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from pythonbpf import bpf, section, bpfglobal, BPF, trace_pipe\n",
|
||||||
|
"from ctypes import c_void_p, c_int64"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 10,
|
||||||
|
"id": "cf1c87aa-e173-4156-8f2d-762225bc6d19",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"@bpf\n",
|
||||||
|
"@section(\"tracepoint/syscalls/sys_enter_clone\")\n",
|
||||||
|
"def hello_world(ctx: c_void_p) -> c_int64:\n",
|
||||||
|
" print(\"Hello, World!\")\n",
|
||||||
|
" return 0\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@bpf\n",
|
||||||
|
"@bpfglobal\n",
|
||||||
|
"def LICENSE() -> str:\n",
|
||||||
|
" return \"GPL\"\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"b = BPF()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "bd81383d-f75a-4269-8451-3d985d85b124",
|
||||||
|
"metadata": {
|
||||||
|
"scrolled": true
|
||||||
|
},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
" Cache2 I/O-4716 [003] ...21 8218.000492: bpf_trace_printk: count: 11 with 4716\n",
|
||||||
|
"\n",
|
||||||
|
" Cache2 I/O-4716 [003] ...21 8218.000499: bpf_trace_printk: Hello, World!\n",
|
||||||
|
"\n",
|
||||||
|
" WebExtensions-5168 [002] ...21 8219.320392: bpf_trace_printk: count: 13 with 5168\n",
|
||||||
|
"\n",
|
||||||
|
" WebExtensions-5168 [002] ...21 8219.320399: bpf_trace_printk: Hello, World!\n",
|
||||||
|
"\n",
|
||||||
|
" python-21155 [001] ...21 8220.933716: bpf_trace_printk: count: 5 with 21155\n",
|
||||||
|
"\n",
|
||||||
|
" python-21155 [001] ...21 8220.933721: bpf_trace_printk: Hello, World!\n",
|
||||||
|
"\n",
|
||||||
|
" python-21155 [002] ...21 8221.341290: bpf_trace_printk: count: 6 with 21155\n",
|
||||||
|
"\n",
|
||||||
|
" python-21155 [002] ...21 8221.341295: bpf_trace_printk: Hello, World!\n",
|
||||||
|
"\n",
|
||||||
|
" Isolated Web Co-5462 [000] ...21 8223.095033: bpf_trace_printk: count: 7 with 5462\n",
|
||||||
|
"\n",
|
||||||
|
" Isolated Web Co-5462 [000] ...21 8223.095043: bpf_trace_printk: Hello, World!\n",
|
||||||
|
"\n",
|
||||||
|
" firefox-4542 [000] ...21 8227.760067: bpf_trace_printk: count: 8 with 4542\n",
|
||||||
|
"\n",
|
||||||
|
" firefox-4542 [000] ...21 8227.760080: bpf_trace_printk: Hello, World!\n",
|
||||||
|
"\n",
|
||||||
|
" Isolated Web Co-12404 [003] ...21 8227.917086: bpf_trace_printk: count: 7 with 12404\n",
|
||||||
|
"\n",
|
||||||
|
" Isolated Web Co-12404 [003] ...21 8227.917095: bpf_trace_printk: Hello, World!\n",
|
||||||
|
"\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"b.load()\n",
|
||||||
|
"b.attach_all()\n",
|
||||||
|
"trace_pipe()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "01e1f25b-decc-425b-a1aa-a5e701082574",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3 (ipykernel)",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.13.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
23
BCC-Examples/hello_world.py
Normal file
23
BCC-Examples/hello_world.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from pythonbpf import bpf, section, bpfglobal, BPF, trace_pipe
|
||||||
|
from ctypes import c_void_p, c_int64
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_clone")
|
||||||
|
def hello_world(ctx: c_void_p) -> c_int64:
|
||||||
|
print("Hello, World!")
|
||||||
|
return 0 # type: ignore [return-value]
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
|
||||||
|
# Compile and load
|
||||||
|
b = BPF()
|
||||||
|
b.load()
|
||||||
|
b.attach_all()
|
||||||
|
|
||||||
|
trace_pipe()
|
||||||
9
BCC-Examples/requirements.txt
Normal file
9
BCC-Examples/requirements.txt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Requirements for PythonBPF BCC-Examples
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
dash
|
||||||
|
matplotlib
|
||||||
|
numpy
|
||||||
|
plotly
|
||||||
|
rich
|
||||||
107
BCC-Examples/sync_count.ipynb
Normal file
107
BCC-Examples/sync_count.ipynb
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "dcab010c-f5e9-446f-9f9f-056cc794ad14",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from pythonbpf import bpf, map, section, bpfglobal, BPF, trace_fields\n",
|
||||||
|
"from pythonbpf.helper import ktime\n",
|
||||||
|
"from pythonbpf.maps import HashMap\n",
|
||||||
|
"\n",
|
||||||
|
"from ctypes import c_void_p, c_int64"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "720797e8-9c81-4af6-a385-80f1ec4c0f15",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"@bpf\n",
|
||||||
|
"@map\n",
|
||||||
|
"def last() -> HashMap:\n",
|
||||||
|
" return HashMap(key=c_int64, value=c_int64, max_entries=2)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@bpf\n",
|
||||||
|
"@section(\"tracepoint/syscalls/sys_enter_sync\")\n",
|
||||||
|
"def do_trace(ctx: c_void_p) -> c_int64:\n",
|
||||||
|
" ts_key, cnt_key = 0, 1\n",
|
||||||
|
" tsp, cntp = last.lookup(ts_key), last.lookup(cnt_key)\n",
|
||||||
|
" if not cntp:\n",
|
||||||
|
" last.update(cnt_key, 0)\n",
|
||||||
|
" cntp = last.lookup(cnt_key)\n",
|
||||||
|
" if tsp:\n",
|
||||||
|
" delta = ktime() - tsp\n",
|
||||||
|
" if delta < 1000000000:\n",
|
||||||
|
" time_ms = delta // 1000000\n",
|
||||||
|
" print(f\"{time_ms} {cntp}\")\n",
|
||||||
|
" last.delete(ts_key)\n",
|
||||||
|
" else:\n",
|
||||||
|
" last.update(ts_key, ktime())\n",
|
||||||
|
" last.update(cnt_key, cntp + 1)\n",
|
||||||
|
" return 0\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@bpf\n",
|
||||||
|
"@bpfglobal\n",
|
||||||
|
"def LICENSE() -> str:\n",
|
||||||
|
" return \"GPL\"\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"# Compile and load\n",
|
||||||
|
"b = BPF()\n",
|
||||||
|
"b.load()\n",
|
||||||
|
"b.attach_all()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "78a8b82c-7c5f-43c1-9de1-cd982a0f345b",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"print(\"Tracing for quick sync's... Ctrl-C to end\")\n",
|
||||||
|
"\n",
|
||||||
|
"# format output\n",
|
||||||
|
"start = 0\n",
|
||||||
|
"while True:\n",
|
||||||
|
" try:\n",
|
||||||
|
" task, pid, cpu, flags, ts, msg = trace_fields()\n",
|
||||||
|
" if start == 0:\n",
|
||||||
|
" start = ts\n",
|
||||||
|
" ts -= start\n",
|
||||||
|
" ms, cnt = msg.split()\n",
|
||||||
|
" print(f\"At time {ts} s: Multiple syncs detected, last {ms} ms ago. Count {cnt}\")\n",
|
||||||
|
" except KeyboardInterrupt:\n",
|
||||||
|
" exit()"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3 (ipykernel)",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.13.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
58
BCC-Examples/sync_count.py
Normal file
58
BCC-Examples/sync_count.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
from pythonbpf import bpf, map, section, bpfglobal, BPF, trace_fields
|
||||||
|
from pythonbpf.helper import ktime
|
||||||
|
from pythonbpf.maps import HashMap
|
||||||
|
|
||||||
|
from ctypes import c_void_p, c_int64
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def last() -> HashMap:
|
||||||
|
return HashMap(key=c_int64, value=c_int64, max_entries=2)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_sync")
|
||||||
|
def do_trace(ctx: c_void_p) -> c_int64:
|
||||||
|
ts_key, cnt_key = 0, 1
|
||||||
|
tsp, cntp = last.lookup(ts_key), last.lookup(cnt_key)
|
||||||
|
if not cntp:
|
||||||
|
last.update(cnt_key, 0)
|
||||||
|
cntp = last.lookup(cnt_key)
|
||||||
|
if tsp:
|
||||||
|
delta = ktime() - tsp
|
||||||
|
if delta < 1000000000:
|
||||||
|
time_ms = delta // 1000000
|
||||||
|
print(f"{time_ms} {cntp}")
|
||||||
|
last.delete(ts_key)
|
||||||
|
else:
|
||||||
|
last.update(ts_key, ktime())
|
||||||
|
last.update(cnt_key, cntp + 1)
|
||||||
|
return 0 # type: ignore [return-value]
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
|
||||||
|
# Compile and load
|
||||||
|
b = BPF()
|
||||||
|
b.load()
|
||||||
|
b.attach_all()
|
||||||
|
|
||||||
|
print("Tracing for quick sync's... Ctrl-C to end")
|
||||||
|
|
||||||
|
# format output
|
||||||
|
start = 0
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
task, pid, cpu, flags, ts, msg = trace_fields()
|
||||||
|
if start == 0:
|
||||||
|
start = ts
|
||||||
|
ts -= start
|
||||||
|
ms, cnt = msg.split()
|
||||||
|
print(f"At time {ts} s: Multiple syncs detected, last {ms} ms ago. Count {cnt}")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
exit()
|
||||||
134
BCC-Examples/sync_perf_output.ipynb
Normal file
134
BCC-Examples/sync_perf_output.ipynb
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "b0d1ab05-0c1f-4578-9c1b-568202b95a5c",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from pythonbpf import bpf, map, struct, section, bpfglobal, BPF\n",
|
||||||
|
"from pythonbpf.helper import ktime\n",
|
||||||
|
"from pythonbpf.maps import HashMap, PerfEventArray\n",
|
||||||
|
"from ctypes import c_void_p, c_int64"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "85e50d0a-f9d8-468f-8e03-f5f7128f05d8",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"@bpf\n",
|
||||||
|
"@struct\n",
|
||||||
|
"class data_t:\n",
|
||||||
|
" ts: c_int64\n",
|
||||||
|
" ms: c_int64\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@bpf\n",
|
||||||
|
"@map\n",
|
||||||
|
"def events() -> PerfEventArray:\n",
|
||||||
|
" return PerfEventArray(key_size=c_int64, value_size=c_int64)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@bpf\n",
|
||||||
|
"@map\n",
|
||||||
|
"def last() -> HashMap:\n",
|
||||||
|
" return HashMap(key=c_int64, value=c_int64, max_entries=1)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@bpf\n",
|
||||||
|
"@section(\"tracepoint/syscalls/sys_enter_sync\")\n",
|
||||||
|
"def do_trace(ctx: c_void_p) -> c_int64:\n",
|
||||||
|
" dat, dat.ts, key = data_t(), ktime(), 0\n",
|
||||||
|
" tsp = last.lookup(key)\n",
|
||||||
|
" if tsp:\n",
|
||||||
|
" delta = ktime() - tsp\n",
|
||||||
|
" if delta < 1000000000:\n",
|
||||||
|
" dat.ms = delta // 1000000\n",
|
||||||
|
" events.output(dat)\n",
|
||||||
|
" last.delete(key)\n",
|
||||||
|
" else:\n",
|
||||||
|
" last.update(key, ktime())\n",
|
||||||
|
" return 0\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@bpf\n",
|
||||||
|
"@bpfglobal\n",
|
||||||
|
"def LICENSE() -> str:\n",
|
||||||
|
" return \"GPL\"\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"# Compile and load\n",
|
||||||
|
"b = BPF()\n",
|
||||||
|
"b.load()\n",
|
||||||
|
"b.attach_all()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "40bb1107-369f-4be7-9f10-37201900c16b",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"print(\"Tracing for quick sync's... Ctrl-C to end\")\n",
|
||||||
|
"\n",
|
||||||
|
"# format output\n",
|
||||||
|
"start = 0\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def callback(cpu, event):\n",
|
||||||
|
" global start\n",
|
||||||
|
" if start == 0:\n",
|
||||||
|
" start = event.ts\n",
|
||||||
|
" event.ts -= start\n",
|
||||||
|
" print(\n",
|
||||||
|
" f\"At time {event.ts / 1e9} s: Multiple sync detected, Last sync: {event.ms} ms ago\"\n",
|
||||||
|
" )\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"perf = b[\"events\"].open_perf_buffer(callback, struct_name=\"data_t\")\n",
|
||||||
|
"print(\"Starting to poll... (Ctrl+C to stop)\")\n",
|
||||||
|
"print(\"Try running: fork() or clone() system calls to trigger events\")\n",
|
||||||
|
"\n",
|
||||||
|
"try:\n",
|
||||||
|
" while True:\n",
|
||||||
|
" b[\"events\"].poll(1000)\n",
|
||||||
|
"except KeyboardInterrupt:\n",
|
||||||
|
" print(\"Stopping...\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "94a588d9-3a40-437c-a35b-fc40410f3eb7",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3 (ipykernel)",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.13.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
75
BCC-Examples/sync_perf_output.py
Normal file
75
BCC-Examples/sync_perf_output.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
from pythonbpf import bpf, map, struct, section, bpfglobal, BPF
|
||||||
|
from pythonbpf.helper import ktime
|
||||||
|
from pythonbpf.maps import HashMap, PerfEventArray
|
||||||
|
from ctypes import c_void_p, c_int64
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class data_t:
|
||||||
|
ts: c_int64
|
||||||
|
ms: c_int64
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def events() -> PerfEventArray:
|
||||||
|
return PerfEventArray(key_size=c_int64, value_size=c_int64)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def last() -> HashMap:
|
||||||
|
return HashMap(key=c_int64, value=c_int64, max_entries=1)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_sync")
|
||||||
|
def do_trace(ctx: c_void_p) -> c_int64:
|
||||||
|
dat, dat.ts, key = data_t(), ktime(), 0
|
||||||
|
tsp = last.lookup(key)
|
||||||
|
if tsp:
|
||||||
|
delta = ktime() - tsp
|
||||||
|
if delta < 1000000000:
|
||||||
|
dat.ms = delta // 1000000
|
||||||
|
events.output(dat)
|
||||||
|
last.delete(key)
|
||||||
|
else:
|
||||||
|
last.update(key, ktime())
|
||||||
|
return 0 # type: ignore [return-value]
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
|
||||||
|
# Compile and load
|
||||||
|
b = BPF()
|
||||||
|
b.load()
|
||||||
|
b.attach_all()
|
||||||
|
|
||||||
|
print("Tracing for quick sync's... Ctrl-C to end")
|
||||||
|
|
||||||
|
# format output
|
||||||
|
start = 0
|
||||||
|
|
||||||
|
|
||||||
|
def callback(cpu, event):
|
||||||
|
global start
|
||||||
|
if start == 0:
|
||||||
|
start = event.ts
|
||||||
|
event.ts -= start
|
||||||
|
print(
|
||||||
|
f"At time {event.ts / 1e9} s: Multiple sync detected, Last sync: {event.ms} ms ago"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
perf = b["events"].open_perf_buffer(callback, struct_name="data_t")
|
||||||
|
print("Starting to poll... (Ctrl+C to stop)")
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
b["events"].poll(1000)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Stopping...")
|
||||||
102
BCC-Examples/sync_timing.ipynb
Normal file
102
BCC-Examples/sync_timing.ipynb
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "bfe01ceb-2f27-41b3-b3ba-50ec65cfddda",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from pythonbpf import bpf, map, section, bpfglobal, BPF, trace_fields\n",
|
||||||
|
"from pythonbpf.helper import ktime\n",
|
||||||
|
"from pythonbpf.maps import HashMap\n",
|
||||||
|
"\n",
|
||||||
|
"from ctypes import c_void_p, c_int64"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "ddb115f4-20a7-43bc-bb5b-ccbfd6031fc2",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"@bpf\n",
|
||||||
|
"@map\n",
|
||||||
|
"def last() -> HashMap:\n",
|
||||||
|
" return HashMap(key=c_int64, value=c_int64, max_entries=1)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@bpf\n",
|
||||||
|
"@section(\"tracepoint/syscalls/sys_enter_sync\")\n",
|
||||||
|
"def do_trace(ctx: c_void_p) -> c_int64:\n",
|
||||||
|
" key = 0\n",
|
||||||
|
" tsp = last.lookup(key)\n",
|
||||||
|
" if tsp:\n",
|
||||||
|
" delta = ktime() - tsp\n",
|
||||||
|
" if delta < 1000000000:\n",
|
||||||
|
" time_ms = delta // 1000000\n",
|
||||||
|
" print(f\"{time_ms}\")\n",
|
||||||
|
" last.delete(key)\n",
|
||||||
|
" else:\n",
|
||||||
|
" last.update(key, ktime())\n",
|
||||||
|
" return 0\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@bpf\n",
|
||||||
|
"@bpfglobal\n",
|
||||||
|
"def LICENSE() -> str:\n",
|
||||||
|
" return \"GPL\"\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"# Compile and load\n",
|
||||||
|
"b = BPF()\n",
|
||||||
|
"b.load()\n",
|
||||||
|
"b.attach_all()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "e4f46574-9fd8-46e7-9c7b-27a36d07f200",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"print(\"Tracing for quick sync's... Ctrl-C to end\")\n",
|
||||||
|
"\n",
|
||||||
|
"# format output\n",
|
||||||
|
"start = 0\n",
|
||||||
|
"while True:\n",
|
||||||
|
" try:\n",
|
||||||
|
" task, pid, cpu, flags, ts, ms = trace_fields()\n",
|
||||||
|
" if start == 0:\n",
|
||||||
|
" start = ts\n",
|
||||||
|
" ts -= start\n",
|
||||||
|
" print(f\"At time {ts} s: Multiple syncs detected, last {ms} ms ago\")\n",
|
||||||
|
" except KeyboardInterrupt:\n",
|
||||||
|
" exit()"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3 (ipykernel)",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.13.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
53
BCC-Examples/sync_timing.py
Normal file
53
BCC-Examples/sync_timing.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
from pythonbpf import bpf, map, section, bpfglobal, BPF, trace_fields
|
||||||
|
from pythonbpf.helper import ktime
|
||||||
|
from pythonbpf.maps import HashMap
|
||||||
|
|
||||||
|
from ctypes import c_void_p, c_int64
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def last() -> HashMap:
|
||||||
|
return HashMap(key=c_int64, value=c_int64, max_entries=1)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_sync")
|
||||||
|
def do_trace(ctx: c_void_p) -> c_int64:
|
||||||
|
key = 0
|
||||||
|
tsp = last.lookup(key)
|
||||||
|
if tsp:
|
||||||
|
delta = ktime() - tsp
|
||||||
|
if delta < 1000000000:
|
||||||
|
time_ms = delta // 1000000
|
||||||
|
print(f"{time_ms}")
|
||||||
|
last.delete(key)
|
||||||
|
else:
|
||||||
|
last.update(key, ktime())
|
||||||
|
return 0 # type: ignore [return-value]
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
|
||||||
|
# Compile and load
|
||||||
|
b = BPF()
|
||||||
|
b.load()
|
||||||
|
b.attach_all()
|
||||||
|
|
||||||
|
print("Tracing for quick sync's... Ctrl-C to end")
|
||||||
|
|
||||||
|
# format output
|
||||||
|
start = 0
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
task, pid, cpu, flags, ts, ms = trace_fields()
|
||||||
|
if start == 0:
|
||||||
|
start = ts
|
||||||
|
ts -= start
|
||||||
|
print(f"At time {ts} s: Multiple syncs detected, last {ms} ms ago")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
exit()
|
||||||
73
BCC-Examples/sys_sync.ipynb
Normal file
73
BCC-Examples/sys_sync.ipynb
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "bb49598f-b9cc-4ea8-8391-923cad513711",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from pythonbpf import bpf, section, bpfglobal, BPF, trace_pipe\n",
|
||||||
|
"from ctypes import c_void_p, c_int64"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "5da237b0-1c7d-4ec5-8c24-696b1c1d97fa",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"@bpf\n",
|
||||||
|
"@section(\"tracepoint/syscalls/sys_enter_sync\")\n",
|
||||||
|
"def hello_world(ctx: c_void_p) -> c_int64:\n",
|
||||||
|
" print(\"sys_sync() called\")\n",
|
||||||
|
" return 0\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@bpf\n",
|
||||||
|
"@bpfglobal\n",
|
||||||
|
"def LICENSE() -> str:\n",
|
||||||
|
" return \"GPL\"\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"# Compile and load\n",
|
||||||
|
"b = BPF()\n",
|
||||||
|
"b.load()\n",
|
||||||
|
"b.attach_all()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "e4c218ac-fe47-4fd1-a27b-c07e02f3cd05",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"print(\"Tracing sys_sync()... Ctrl-C to end.\")\n",
|
||||||
|
"trace_pipe()"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3 (ipykernel)",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.13.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
23
BCC-Examples/sys_sync.py
Normal file
23
BCC-Examples/sys_sync.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from pythonbpf import bpf, section, bpfglobal, BPF, trace_pipe
|
||||||
|
from ctypes import c_void_p, c_int64
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_sync")
|
||||||
|
def hello_world(ctx: c_void_p) -> c_int64:
|
||||||
|
print("sys_sync() called")
|
||||||
|
return c_int64(0)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
|
||||||
|
# Compile and load
|
||||||
|
b = BPF()
|
||||||
|
b.load()
|
||||||
|
b.attach_all()
|
||||||
|
print("Tracing sys_sync()... Ctrl-C to end.")
|
||||||
|
trace_pipe()
|
||||||
252
BCC-Examples/vfsreadlat.ipynb
Normal file
252
BCC-Examples/vfsreadlat.ipynb
Normal file
File diff suppressed because one or more lines are too long
127
BCC-Examples/vfsreadlat.py
Normal file
127
BCC-Examples/vfsreadlat.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
from pythonbpf import bpf, map, struct, section, bpfglobal, BPF
|
||||||
|
from pythonbpf.helper import ktime, pid
|
||||||
|
from pythonbpf.maps import HashMap, PerfEventArray
|
||||||
|
from ctypes import c_void_p, c_uint64
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class latency_event:
|
||||||
|
pid: c_uint64
|
||||||
|
delta_us: c_uint64 # Latency in microseconds
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def start() -> HashMap:
|
||||||
|
return HashMap(key=c_uint64, value=c_uint64, max_entries=10240)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def events() -> PerfEventArray:
|
||||||
|
return PerfEventArray(key_size=c_uint64, value_size=c_uint64)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("kprobe/vfs_read")
|
||||||
|
def do_entry(ctx: c_void_p) -> c_uint64:
|
||||||
|
p, ts = pid(), ktime()
|
||||||
|
start.update(p, ts)
|
||||||
|
return 0 # type: ignore [return-value]
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("kretprobe/vfs_read")
|
||||||
|
def do_return(ctx: c_void_p) -> c_uint64:
|
||||||
|
p = pid()
|
||||||
|
tsp = start.lookup(p)
|
||||||
|
|
||||||
|
if tsp:
|
||||||
|
delta_ns = ktime() - tsp
|
||||||
|
|
||||||
|
# Only track if latency > 1 microsecond
|
||||||
|
if delta_ns > 1000:
|
||||||
|
evt = latency_event()
|
||||||
|
evt.pid, evt.delta_us = p, delta_ns // 1000
|
||||||
|
events.output(evt)
|
||||||
|
|
||||||
|
start.delete(p)
|
||||||
|
|
||||||
|
return 0 # type: ignore [return-value]
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
|
||||||
|
# Load BPF
|
||||||
|
print("Loading BPF program...")
|
||||||
|
b = BPF()
|
||||||
|
b.load()
|
||||||
|
b.attach_all()
|
||||||
|
|
||||||
|
# Collect latencies
|
||||||
|
latencies = []
|
||||||
|
|
||||||
|
|
||||||
|
def callback(cpu, event):
|
||||||
|
latencies.append(event.delta_us)
|
||||||
|
|
||||||
|
|
||||||
|
b["events"].open_perf_buffer(callback, struct_name="latency_event")
|
||||||
|
|
||||||
|
print("Tracing vfs_read latency... Hit Ctrl-C to end.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
b["events"].poll(1000)
|
||||||
|
if len(latencies) > 0 and len(latencies) % 1000 == 0:
|
||||||
|
print(f"Collected {len(latencies)} samples...")
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print(f"Collected {len(latencies)} samples. Generating histogram...")
|
||||||
|
|
||||||
|
# Create histogram with matplotlib
|
||||||
|
if latencies:
|
||||||
|
# Use log scale for better visualization
|
||||||
|
log_latencies = np.log2(latencies)
|
||||||
|
|
||||||
|
plt.figure(figsize=(12, 6))
|
||||||
|
|
||||||
|
# Plot 1: Linear histogram
|
||||||
|
plt.subplot(1, 2, 1)
|
||||||
|
plt.hist(latencies, bins=50, edgecolor="black", alpha=0.7)
|
||||||
|
plt.xlabel("Latency (microseconds)")
|
||||||
|
plt.ylabel("Count")
|
||||||
|
plt.title("VFS Read Latency Distribution (Linear)")
|
||||||
|
plt.grid(True, alpha=0.3)
|
||||||
|
|
||||||
|
# Plot 2: Log2 histogram (like BCC)
|
||||||
|
plt.subplot(1, 2, 2)
|
||||||
|
plt.hist(log_latencies, bins=50, edgecolor="black", alpha=0.7, color="orange")
|
||||||
|
plt.xlabel("log2(Latency in µs)")
|
||||||
|
plt.ylabel("Count")
|
||||||
|
plt.title("VFS Read Latency Distribution (Log2)")
|
||||||
|
plt.grid(True, alpha=0.3)
|
||||||
|
|
||||||
|
# Add statistics
|
||||||
|
print("Statistics:")
|
||||||
|
print(f" Count: {len(latencies)}")
|
||||||
|
print(f" Min: {min(latencies)} µs")
|
||||||
|
print(f" Max: {max(latencies)} µs")
|
||||||
|
print(f" Mean: {np.mean(latencies):.2f} µs")
|
||||||
|
print(f" Median: {np.median(latencies):.2f} µs")
|
||||||
|
print(f" P95: {np.percentile(latencies, 95):.2f} µs")
|
||||||
|
print(f" P99: {np.percentile(latencies, 99):.2f} µs")
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig("vfs_read_latency.png", dpi=150)
|
||||||
|
print("Histogram saved to vfs_read_latency.png")
|
||||||
|
plt.show()
|
||||||
|
else:
|
||||||
|
print("No samples collected!")
|
||||||
101
BCC-Examples/vfsreadlat_plotly/bpf_program.py
Normal file
101
BCC-Examples/vfsreadlat_plotly/bpf_program.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
"""BPF program for tracing VFS read latency."""
|
||||||
|
|
||||||
|
from pythonbpf import bpf, map, struct, section, bpfglobal, BPF
|
||||||
|
from pythonbpf.helper import ktime, pid
|
||||||
|
from pythonbpf.maps import HashMap, PerfEventArray
|
||||||
|
from ctypes import c_void_p, c_uint64
|
||||||
|
import argparse
|
||||||
|
from data_collector import LatencyCollector
|
||||||
|
from dashboard import LatencyDashboard
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class latency_event:
|
||||||
|
pid: c_uint64
|
||||||
|
delta_us: c_uint64
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def start() -> HashMap:
|
||||||
|
"""Map to store start timestamps by PID."""
|
||||||
|
return HashMap(key=c_uint64, value=c_uint64, max_entries=10240)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def events() -> PerfEventArray:
|
||||||
|
"""Perf event array for sending latency events to userspace."""
|
||||||
|
return PerfEventArray(key_size=c_uint64, value_size=c_uint64)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("kprobe/vfs_read")
|
||||||
|
def do_entry(ctx: c_void_p) -> c_uint64:
|
||||||
|
"""Record start time when vfs_read is called."""
|
||||||
|
p, ts = pid(), ktime()
|
||||||
|
start.update(p, ts)
|
||||||
|
return 0 # type: ignore [return-value]
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("kretprobe/vfs_read")
|
||||||
|
def do_return(ctx: c_void_p) -> c_uint64:
|
||||||
|
"""Calculate and record latency when vfs_read returns."""
|
||||||
|
p = pid()
|
||||||
|
tsp = start.lookup(p)
|
||||||
|
|
||||||
|
if tsp:
|
||||||
|
delta_ns = ktime() - tsp
|
||||||
|
|
||||||
|
# Only track latencies > 1 microsecond
|
||||||
|
if delta_ns > 1000:
|
||||||
|
evt = latency_event()
|
||||||
|
evt.pid, evt.delta_us = p, delta_ns // 1000
|
||||||
|
events.output(evt)
|
||||||
|
|
||||||
|
start.delete(p)
|
||||||
|
|
||||||
|
return 0 # type: ignore [return-value]
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
"""Parse command line arguments."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Monitor VFS read latency with live dashboard"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--host", default="0.0.0.0", help="Dashboard host (default: 0.0.0.0)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--port", type=int, default=8050, help="Dashboard port (default: 8050)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--buffer", type=int, default=10000, help="Recent data buffer size"
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
# Load BPF program
|
||||||
|
print("Loading BPF program...")
|
||||||
|
b = BPF()
|
||||||
|
b.load()
|
||||||
|
b.attach_all()
|
||||||
|
print("✅ BPF program loaded and attached")
|
||||||
|
|
||||||
|
# Setup data collector
|
||||||
|
collector = LatencyCollector(b, buffer_size=args.buffer)
|
||||||
|
collector.start()
|
||||||
|
|
||||||
|
# Create and run dashboard
|
||||||
|
dashboard = LatencyDashboard(collector)
|
||||||
|
dashboard.run(host=args.host, port=args.port)
|
||||||
282
BCC-Examples/vfsreadlat_plotly/dashboard.py
Normal file
282
BCC-Examples/vfsreadlat_plotly/dashboard.py
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
"""Plotly Dash dashboard for visualizing latency data."""
|
||||||
|
|
||||||
|
import dash
|
||||||
|
from dash import dcc, html
|
||||||
|
from dash.dependencies import Input, Output
|
||||||
|
import plotly.graph_objects as go
|
||||||
|
from plotly.subplots import make_subplots
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
class LatencyDashboard:
|
||||||
|
"""Interactive dashboard for latency visualization."""
|
||||||
|
|
||||||
|
def __init__(self, collector, title: str = "VFS Read Latency Monitor"):
|
||||||
|
self.collector = collector
|
||||||
|
self.app = dash.Dash(__name__)
|
||||||
|
self.app.title = title
|
||||||
|
self._setup_layout()
|
||||||
|
self._setup_callbacks()
|
||||||
|
|
||||||
|
def _setup_layout(self):
|
||||||
|
"""Create dashboard layout."""
|
||||||
|
self.app.layout = html.Div(
|
||||||
|
[
|
||||||
|
html.H1(
|
||||||
|
"🔥 VFS Read Latency Dashboard",
|
||||||
|
style={
|
||||||
|
"textAlign": "center",
|
||||||
|
"color": "#2c3e50",
|
||||||
|
"marginBottom": 20,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
# Stats cards
|
||||||
|
html.Div(
|
||||||
|
[
|
||||||
|
self._create_stat_card(
|
||||||
|
"total-samples", "📊 Total Samples", "#3498db"
|
||||||
|
),
|
||||||
|
self._create_stat_card(
|
||||||
|
"mean-latency", "⚡ Mean Latency", "#e74c3c"
|
||||||
|
),
|
||||||
|
self._create_stat_card(
|
||||||
|
"p99-latency", "🔥 P99 Latency", "#f39c12"
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style={
|
||||||
|
"display": "flex",
|
||||||
|
"justifyContent": "space-around",
|
||||||
|
"marginBottom": 30,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
# Graphs - ✅ Make sure these IDs match the callback outputs
|
||||||
|
dcc.Graph(id="dual-histogram", style={"height": "450px"}),
|
||||||
|
dcc.Graph(id="log2-buckets", style={"height": "350px"}),
|
||||||
|
dcc.Graph(id="timeseries-graph", style={"height": "300px"}),
|
||||||
|
# Auto-update
|
||||||
|
dcc.Interval(id="interval-component", interval=1000, n_intervals=0),
|
||||||
|
],
|
||||||
|
style={"padding": 20, "fontFamily": "Arial, sans-serif"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_stat_card(self, id_name: str, title: str, color: str):
|
||||||
|
"""Create a statistics card."""
|
||||||
|
return html.Div(
|
||||||
|
[
|
||||||
|
html.H3(title, style={"color": color}),
|
||||||
|
html.H2(id=id_name, style={"fontSize": 48, "color": "#2c3e50"}),
|
||||||
|
],
|
||||||
|
className="stat-box",
|
||||||
|
style={
|
||||||
|
"background": "white",
|
||||||
|
"padding": 20,
|
||||||
|
"borderRadius": 10,
|
||||||
|
"boxShadow": "0 4px 6px rgba(0,0,0,0.1)",
|
||||||
|
"textAlign": "center",
|
||||||
|
"flex": 1,
|
||||||
|
"margin": "0 10px",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _setup_callbacks(self):
|
||||||
|
"""Setup dashboard callbacks."""
|
||||||
|
|
||||||
|
@self.app.callback(
|
||||||
|
[
|
||||||
|
Output("total-samples", "children"),
|
||||||
|
Output("mean-latency", "children"),
|
||||||
|
Output("p99-latency", "children"),
|
||||||
|
Output("dual-histogram", "figure"), # ✅ Match layout IDs
|
||||||
|
Output("log2-buckets", "figure"), # ✅ Match layout IDs
|
||||||
|
Output("timeseries-graph", "figure"), # ✅ Match layout IDs
|
||||||
|
],
|
||||||
|
[Input("interval-component", "n_intervals")],
|
||||||
|
)
|
||||||
|
def update_dashboard(n):
|
||||||
|
stats = self.collector.get_stats()
|
||||||
|
|
||||||
|
if stats.total == 0:
|
||||||
|
return self._empty_state()
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"{stats.total:,}",
|
||||||
|
f"{stats.mean:.1f} µs",
|
||||||
|
f"{stats.p99:.1f} µs",
|
||||||
|
self._create_dual_histogram(),
|
||||||
|
self._create_log2_buckets(),
|
||||||
|
self._create_timeseries(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _empty_state(self):
|
||||||
|
"""Return empty state for dashboard."""
|
||||||
|
empty_fig = go.Figure()
|
||||||
|
empty_fig.update_layout(
|
||||||
|
title="Waiting for data... Generate some disk I/O!", template="plotly_white"
|
||||||
|
)
|
||||||
|
# ✅ Return 6 values (3 stats + 3 figures)
|
||||||
|
return "0", "0 µs", "0 µs", empty_fig, empty_fig, empty_fig
|
||||||
|
|
||||||
|
def _create_dual_histogram(self) -> go.Figure:
|
||||||
|
"""Create side-by-side linear and log2 histograms."""
|
||||||
|
latencies = self.collector.get_all_latencies()
|
||||||
|
|
||||||
|
# Create subplots
|
||||||
|
fig = make_subplots(
|
||||||
|
rows=1,
|
||||||
|
cols=2,
|
||||||
|
subplot_titles=("Linear Scale", "Log2 Scale"),
|
||||||
|
horizontal_spacing=0.12,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Linear histogram
|
||||||
|
fig.add_trace(
|
||||||
|
go.Histogram(
|
||||||
|
x=latencies,
|
||||||
|
nbinsx=50,
|
||||||
|
marker_color="rgb(55, 83, 109)",
|
||||||
|
opacity=0.75,
|
||||||
|
name="Linear",
|
||||||
|
),
|
||||||
|
row=1,
|
||||||
|
col=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Log2 histogram
|
||||||
|
log2_latencies = np.log2(latencies + 1) # +1 to avoid log2(0)
|
||||||
|
fig.add_trace(
|
||||||
|
go.Histogram(
|
||||||
|
x=log2_latencies,
|
||||||
|
nbinsx=30,
|
||||||
|
marker_color="rgb(243, 156, 18)",
|
||||||
|
opacity=0.75,
|
||||||
|
name="Log2",
|
||||||
|
),
|
||||||
|
row=1,
|
||||||
|
col=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update axes
|
||||||
|
fig.update_xaxes(title_text="Latency (µs)", row=1, col=1)
|
||||||
|
fig.update_xaxes(title_text="log2(Latency in µs)", row=1, col=2)
|
||||||
|
fig.update_yaxes(title_text="Count", row=1, col=1)
|
||||||
|
fig.update_yaxes(title_text="Count", row=1, col=2)
|
||||||
|
|
||||||
|
fig.update_layout(
|
||||||
|
title_text="📊 Latency Distribution (Linear vs Log2)",
|
||||||
|
template="plotly_white",
|
||||||
|
showlegend=False,
|
||||||
|
height=450,
|
||||||
|
)
|
||||||
|
|
||||||
|
return fig
|
||||||
|
|
||||||
|
def _create_log2_buckets(self) -> go.Figure:
|
||||||
|
"""Create bar chart of log2 buckets (like BCC histogram)."""
|
||||||
|
buckets = self.collector.get_histogram_buckets()
|
||||||
|
|
||||||
|
if not buckets:
|
||||||
|
fig = go.Figure()
|
||||||
|
fig.update_layout(
|
||||||
|
title="🔥 Log2 Histogram - Waiting for data...", template="plotly_white"
|
||||||
|
)
|
||||||
|
return fig
|
||||||
|
|
||||||
|
# Sort buckets
|
||||||
|
sorted_buckets = sorted(buckets.keys())
|
||||||
|
counts = [buckets[b] for b in sorted_buckets]
|
||||||
|
|
||||||
|
# Create labels (e.g., "8-16µs", "16-32µs")
|
||||||
|
labels = []
|
||||||
|
hover_text = []
|
||||||
|
for bucket in sorted_buckets:
|
||||||
|
lower = 2**bucket
|
||||||
|
upper = 2 ** (bucket + 1)
|
||||||
|
labels.append(f"{lower}-{upper}")
|
||||||
|
|
||||||
|
# Calculate percentage
|
||||||
|
total = sum(counts)
|
||||||
|
pct = (buckets[bucket] / total) * 100 if total > 0 else 0
|
||||||
|
hover_text.append(
|
||||||
|
f"Range: {lower}-{upper} µs<br>"
|
||||||
|
f"Count: {buckets[bucket]:,}<br>"
|
||||||
|
f"Percentage: {pct:.2f}%"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create bar chart
|
||||||
|
fig = go.Figure()
|
||||||
|
|
||||||
|
fig.add_trace(
|
||||||
|
go.Bar(
|
||||||
|
x=labels,
|
||||||
|
y=counts,
|
||||||
|
marker=dict(
|
||||||
|
color=counts,
|
||||||
|
colorscale="YlOrRd",
|
||||||
|
showscale=True,
|
||||||
|
colorbar=dict(title="Count"),
|
||||||
|
),
|
||||||
|
text=counts,
|
||||||
|
textposition="outside",
|
||||||
|
hovertext=hover_text,
|
||||||
|
hoverinfo="text",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
fig.update_layout(
|
||||||
|
title="🔥 Log2 Histogram (BCC-style buckets)",
|
||||||
|
xaxis_title="Latency Range (µs)",
|
||||||
|
yaxis_title="Count",
|
||||||
|
template="plotly_white",
|
||||||
|
height=350,
|
||||||
|
xaxis=dict(tickangle=-45),
|
||||||
|
)
|
||||||
|
|
||||||
|
return fig
|
||||||
|
|
||||||
|
def _create_timeseries(self) -> go.Figure:
|
||||||
|
"""Create time series figure."""
|
||||||
|
recent = self.collector.get_recent_latencies()
|
||||||
|
|
||||||
|
if not recent:
|
||||||
|
fig = go.Figure()
|
||||||
|
fig.update_layout(
|
||||||
|
title="⏱️ Real-time Latency - Waiting for data...",
|
||||||
|
template="plotly_white",
|
||||||
|
)
|
||||||
|
return fig
|
||||||
|
|
||||||
|
times = [d["time"] for d in recent]
|
||||||
|
lats = [d["latency"] for d in recent]
|
||||||
|
|
||||||
|
fig = go.Figure()
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=times,
|
||||||
|
y=lats,
|
||||||
|
mode="lines",
|
||||||
|
line=dict(color="rgb(231, 76, 60)", width=2),
|
||||||
|
fill="tozeroy",
|
||||||
|
fillcolor="rgba(231, 76, 60, 0.2)",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
fig.update_layout(
|
||||||
|
title="⏱️ Real-time Latency (Last 10,000 samples)",
|
||||||
|
xaxis_title="Time (seconds)",
|
||||||
|
yaxis_title="Latency (µs)",
|
||||||
|
template="plotly_white",
|
||||||
|
height=300,
|
||||||
|
)
|
||||||
|
|
||||||
|
return fig
|
||||||
|
|
||||||
|
def run(self, host: str = "0.0.0.0", port: int = 8050, debug: bool = False):
|
||||||
|
"""Run the dashboard server."""
|
||||||
|
print(f"\n{'=' * 60}")
|
||||||
|
print(f"🚀 Dashboard running at: http://{host}:{port}")
|
||||||
|
print(" Access from your browser to see live graphs")
|
||||||
|
print(
|
||||||
|
" Generate disk I/O to see data: dd if=/dev/zero of=/tmp/test bs=1M count=100"
|
||||||
|
)
|
||||||
|
print(f"{'=' * 60}\n")
|
||||||
|
self.app.run(debug=debug, host=host, port=port)
|
||||||
96
BCC-Examples/vfsreadlat_plotly/data_collector.py
Normal file
96
BCC-Examples/vfsreadlat_plotly/data_collector.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
"""Data collection and management."""
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import numpy as np
|
||||||
|
from collections import deque
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List, Dict
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LatencyStats:
|
||||||
|
"""Statistics computed from latency data."""
|
||||||
|
|
||||||
|
total: int = 0
|
||||||
|
mean: float = 0.0
|
||||||
|
median: float = 0.0
|
||||||
|
min: float = 0.0
|
||||||
|
max: float = 0.0
|
||||||
|
p95: float = 0.0
|
||||||
|
p99: float = 0.0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_array(cls, data: np.ndarray) -> "LatencyStats":
|
||||||
|
"""Compute stats from numpy array."""
|
||||||
|
if len(data) == 0:
|
||||||
|
return cls()
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
total=len(data),
|
||||||
|
mean=float(np.mean(data)),
|
||||||
|
median=float(np.median(data)),
|
||||||
|
min=float(np.min(data)),
|
||||||
|
max=float(np.max(data)),
|
||||||
|
p95=float(np.percentile(data, 95)),
|
||||||
|
p99=float(np.percentile(data, 99)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LatencyCollector:
|
||||||
|
"""Collects and manages latency data from BPF."""
|
||||||
|
|
||||||
|
def __init__(self, bpf_object, buffer_size: int = 10000):
|
||||||
|
self.bpf = bpf_object
|
||||||
|
self.all_latencies: List[float] = []
|
||||||
|
self.recent_latencies = deque(maxlen=buffer_size) # type: ignore [var-annotated]
|
||||||
|
self.start_time = time.time()
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._poll_thread = None
|
||||||
|
|
||||||
|
def callback(self, cpu: int, event):
|
||||||
|
"""Callback for BPF events."""
|
||||||
|
with self._lock:
|
||||||
|
self.all_latencies.append(event.delta_us)
|
||||||
|
self.recent_latencies.append(
|
||||||
|
{"time": time.time() - self.start_time, "latency": event.delta_us}
|
||||||
|
)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Start collecting data."""
|
||||||
|
self.bpf["events"].open_perf_buffer(self.callback, struct_name="latency_event")
|
||||||
|
|
||||||
|
def poll_loop():
|
||||||
|
while True:
|
||||||
|
self.bpf["events"].poll(100)
|
||||||
|
|
||||||
|
self._poll_thread = threading.Thread(target=poll_loop, daemon=True)
|
||||||
|
self._poll_thread.start()
|
||||||
|
print("✅ Data collection started")
|
||||||
|
|
||||||
|
def get_all_latencies(self) -> np.ndarray:
|
||||||
|
"""Get all latencies as numpy array."""
|
||||||
|
with self._lock:
|
||||||
|
return np.array(self.all_latencies) if self.all_latencies else np.array([])
|
||||||
|
|
||||||
|
def get_recent_latencies(self) -> List[Dict]:
|
||||||
|
"""Get recent latencies with timestamps."""
|
||||||
|
with self._lock:
|
||||||
|
return list(self.recent_latencies)
|
||||||
|
|
||||||
|
def get_stats(self) -> LatencyStats:
|
||||||
|
"""Compute current statistics."""
|
||||||
|
return LatencyStats.from_array(self.get_all_latencies())
|
||||||
|
|
||||||
|
def get_histogram_buckets(self) -> Dict[int, int]:
|
||||||
|
"""Get log2 histogram buckets."""
|
||||||
|
latencies = self.get_all_latencies()
|
||||||
|
if len(latencies) == 0:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
log_buckets = np.floor(np.log2(latencies + 1)).astype(int)
|
||||||
|
buckets = {} # type: ignore [var-annotated]
|
||||||
|
for bucket in log_buckets:
|
||||||
|
buckets[bucket] = buckets.get(bucket, 0) + 1
|
||||||
|
|
||||||
|
return buckets
|
||||||
178
BCC-Examples/vfsreadlat_rich.py
Normal file
178
BCC-Examples/vfsreadlat_rich.py
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
from pythonbpf import bpf, map, struct, section, bpfglobal, BPF
|
||||||
|
from pythonbpf.helper import ktime, pid
|
||||||
|
from pythonbpf.maps import HashMap, PerfEventArray
|
||||||
|
from ctypes import c_void_p, c_uint64
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.live import Live
|
||||||
|
from rich.table import Table
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.layout import Layout
|
||||||
|
import numpy as np
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
# ==================== BPF Setup ====================
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class latency_event:
|
||||||
|
pid: c_uint64
|
||||||
|
delta_us: c_uint64
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def start() -> HashMap:
|
||||||
|
return HashMap(key=c_uint64, value=c_uint64, max_entries=10240)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def events() -> PerfEventArray:
|
||||||
|
return PerfEventArray(key_size=c_uint64, value_size=c_uint64)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("kprobe/vfs_read")
|
||||||
|
def do_entry(ctx: c_void_p) -> c_uint64:
|
||||||
|
p, ts = pid(), ktime()
|
||||||
|
start.update(p, ts)
|
||||||
|
return 0 # type: ignore [return-value]
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("kretprobe/vfs_read")
|
||||||
|
def do_return(ctx: c_void_p) -> c_uint64:
|
||||||
|
p = pid()
|
||||||
|
tsp = start.lookup(p)
|
||||||
|
|
||||||
|
if tsp:
|
||||||
|
delta_ns = ktime() - tsp
|
||||||
|
|
||||||
|
if delta_ns > 1000:
|
||||||
|
evt = latency_event()
|
||||||
|
evt.pid, evt.delta_us = p, delta_ns // 1000
|
||||||
|
events.output(evt)
|
||||||
|
|
||||||
|
start.delete(p)
|
||||||
|
|
||||||
|
return 0 # type: ignore [return-value]
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
console.print("[bold green]Loading BPF program...[/]")
|
||||||
|
|
||||||
|
b = BPF()
|
||||||
|
b.load()
|
||||||
|
b.attach_all()
|
||||||
|
|
||||||
|
# ==================== Data Collection ====================
|
||||||
|
|
||||||
|
all_latencies = []
|
||||||
|
histogram_buckets = Counter() # type: ignore [var-annotated]
|
||||||
|
|
||||||
|
|
||||||
|
def callback(cpu, event):
|
||||||
|
all_latencies.append(event.delta_us)
|
||||||
|
# Create log2 bucket
|
||||||
|
bucket = int(np.floor(np.log2(event.delta_us + 1)))
|
||||||
|
histogram_buckets[bucket] += 1
|
||||||
|
|
||||||
|
|
||||||
|
b["events"].open_perf_buffer(callback, struct_name="latency_event")
|
||||||
|
|
||||||
|
|
||||||
|
def poll_events():
|
||||||
|
while True:
|
||||||
|
b["events"].poll(100)
|
||||||
|
|
||||||
|
|
||||||
|
poll_thread = threading.Thread(target=poll_events, daemon=True)
|
||||||
|
poll_thread.start()
|
||||||
|
|
||||||
|
# ==================== Live Display ====================
|
||||||
|
|
||||||
|
|
||||||
|
def generate_display():
|
||||||
|
layout = Layout()
|
||||||
|
layout.split_column(
|
||||||
|
Layout(name="header", size=3),
|
||||||
|
Layout(name="stats", size=8),
|
||||||
|
Layout(name="histogram", size=20),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Header
|
||||||
|
layout["header"].update(
|
||||||
|
Panel("[bold cyan]🔥 VFS Read Latency Monitor[/]", style="bold white on blue")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Stats
|
||||||
|
if len(all_latencies) > 0:
|
||||||
|
lats = np.array(all_latencies)
|
||||||
|
stats_table = Table(show_header=False, box=None, padding=(0, 2))
|
||||||
|
stats_table.add_column(style="bold cyan")
|
||||||
|
stats_table.add_column(style="bold yellow")
|
||||||
|
|
||||||
|
stats_table.add_row("📊 Total Samples:", f"{len(lats):,}")
|
||||||
|
stats_table.add_row("⚡ Mean Latency:", f"{np.mean(lats):.2f} µs")
|
||||||
|
stats_table.add_row("📉 Min Latency:", f"{np.min(lats):.2f} µs")
|
||||||
|
stats_table.add_row("📈 Max Latency:", f"{np.max(lats):.2f} µs")
|
||||||
|
stats_table.add_row("🎯 P95 Latency:", f"{np.percentile(lats, 95):.2f} µs")
|
||||||
|
stats_table.add_row("🔥 P99 Latency:", f"{np.percentile(lats, 99):.2f} µs")
|
||||||
|
|
||||||
|
layout["stats"].update(
|
||||||
|
Panel(stats_table, title="Statistics", border_style="green")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
layout["stats"].update(
|
||||||
|
Panel("[yellow]Waiting for data...[/]", border_style="yellow")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Histogram
|
||||||
|
if histogram_buckets:
|
||||||
|
hist_table = Table(title="Latency Distribution", box=None)
|
||||||
|
hist_table.add_column("Range", style="cyan", no_wrap=True)
|
||||||
|
hist_table.add_column("Count", justify="right", style="yellow")
|
||||||
|
hist_table.add_column("Distribution", style="green")
|
||||||
|
|
||||||
|
max_count = max(histogram_buckets.values())
|
||||||
|
|
||||||
|
for bucket in sorted(histogram_buckets.keys()):
|
||||||
|
count = histogram_buckets[bucket]
|
||||||
|
lower = 2**bucket
|
||||||
|
upper = 2 ** (bucket + 1)
|
||||||
|
|
||||||
|
# Create bar
|
||||||
|
bar_width = int((count / max_count) * 40)
|
||||||
|
bar = "█" * bar_width
|
||||||
|
|
||||||
|
hist_table.add_row(
|
||||||
|
f"{lower:5d}-{upper:5d} µs",
|
||||||
|
f"{count:6d}",
|
||||||
|
f"[green]{bar}[/] {count / len(all_latencies) * 100:.1f}%",
|
||||||
|
)
|
||||||
|
|
||||||
|
layout["histogram"].update(Panel(hist_table, border_style="green"))
|
||||||
|
|
||||||
|
return layout
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
with Live(generate_display(), refresh_per_second=2, console=console) as live:
|
||||||
|
while True:
|
||||||
|
time.sleep(0.5)
|
||||||
|
live.update(generate_display())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
console.print("\n[bold red]Stopping...[/]")
|
||||||
|
|
||||||
|
if all_latencies:
|
||||||
|
console.print(f"\n[bold green]✅ Collected {len(all_latencies):,} samples[/]")
|
||||||
1
LICENSE
1
LICENSE
@ -200,4 +200,3 @@
|
|||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
|
|
||||||
|
|||||||
8
Makefile
8
Makefile
@ -1,14 +1,10 @@
|
|||||||
compile:
|
install:
|
||||||
chmod +x ./tools/compile.py
|
|
||||||
./tools/compile.py ./examples/execve3.py
|
|
||||||
|
|
||||||
install:
|
|
||||||
pip install -e .
|
pip install -e .
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf build dist *.egg-info
|
rm -rf build dist *.egg-info
|
||||||
rm -rf examples/*.ll examples/*.o
|
rm -rf examples/*.ll examples/*.o
|
||||||
|
|
||||||
all: install compile
|
all: clean install
|
||||||
|
|
||||||
.PHONY: all clean
|
.PHONY: all clean
|
||||||
|
|||||||
252
README.md
252
README.md
@ -1,77 +1,235 @@
|
|||||||
# 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">
|
<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>
|
<!-- PyPI -->
|
||||||
<a href="https://pypi.org/project/pythonbpf"><img src="https://badge.fury.io/py/pythonbpf.svg"></a>
|
<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>
|
</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
|
> **Note**: This project is under active development and not ready for production use.
|
||||||
[Video demo for code under demo/](https://youtu.be/eMyLW8iWbks)
|
|
||||||
|
|
||||||
## Slide Deck
|
---
|
||||||
[Slide deck explaining the project](https://docs.google.com/presentation/d/1DsWDIVrpJhM4RgOETO9VWqUtEHo3-c7XIWmNpi6sTSo/edit?usp=sharing)
|
|
||||||
|
|
||||||
## Installation
|
## Overview
|
||||||
- Have `clang` installed.
|
|
||||||
- `pip install pythonbpf`
|
* 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:
|
||||||
|
|
||||||
|
* `bpftool`
|
||||||
|
* `clang`
|
||||||
|
* Python ≥ 3.8
|
||||||
|
|
||||||
|
Install via pip:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install pythonbpf pylibbpf
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Try It Out!
|
||||||
|
|
||||||
|
#### First, generate the vmlinux.py file for your kernel:
|
||||||
|
- Install the required dependencies:
|
||||||
|
- On Ubuntu:
|
||||||
|
```bash
|
||||||
|
sudo apt-get install bpftool clang
|
||||||
|
pip install pythonbpf pylibbpf ctypeslib2
|
||||||
|
```
|
||||||
|
- Generate the `vmlinux.py` using:
|
||||||
|
```bash
|
||||||
|
sudo tools/vmlinux-gen.py
|
||||||
|
```
|
||||||
|
- Copy this file to `BCC-Examples/`
|
||||||
|
|
||||||
|
#### Next, install requirements for BCC-Examples:
|
||||||
|
- These requirements are only required for the python notebooks, vfsreadlat and container-monitor examples.
|
||||||
|
```bash
|
||||||
|
pip install -r BCC-Examples/requirements.txt
|
||||||
|
```
|
||||||
|
- Now, follow the instructions in the [BCC-Examples/README.md](https://github.com/pythonbpf/Python-BPF/blob/master/BCC-Examples/README.md) to run the examples.
|
||||||
|
|
||||||
|
|
||||||
|
#### To spin up jupyter notebook examples:
|
||||||
|
- Run and follow the instructions on screen
|
||||||
|
```bash
|
||||||
|
curl -s https://raw.githubusercontent.com/pythonbpf/Python-BPF/refs/heads/master/tools/setup.sh | sudo bash
|
||||||
|
```
|
||||||
|
- Check the jupyter server on the web browser and run the notebooks in the `BCC-Examples/` folder.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
## Usage
|
|
||||||
```python
|
```python
|
||||||
# pythonbpf_example.py
|
import time
|
||||||
from pythonbpf import bpf, map, bpfglobal, section, compile
|
from pythonbpf import bpf, map, section, bpfglobal, BPF
|
||||||
from pythonbpf.helpers import bpf_ktime_get_ns
|
from pythonbpf.helper import pid
|
||||||
from pythonbpf.maps import HashMap
|
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
|
@bpf
|
||||||
@map
|
@map
|
||||||
def last() -> HashMap:
|
def hist() -> HashMap:
|
||||||
return HashMap(key=c_uint64, value=c_uint64, max_entries=1)
|
return HashMap(key=c_int32, value=c_uint64, max_entries=4096)
|
||||||
|
|
||||||
|
|
||||||
@bpf
|
@bpf
|
||||||
@section("tracepoint/syscalls/sys_enter_execve")
|
@section("tracepoint/syscalls/sys_enter_clone")
|
||||||
def hello(ctx: c_void_p) -> c_int32:
|
def hello(ctx: c_void_p) -> c_int64:
|
||||||
print("entered")
|
process_id = pid()
|
||||||
return c_int32(0)
|
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 0
|
||||||
|
else:
|
||||||
|
hist.update(process_id, 1)
|
||||||
|
return 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()
|
|
||||||
return c_int64(0)
|
|
||||||
|
|
||||||
@bpf
|
@bpf
|
||||||
@bpfglobal
|
@bpfglobal
|
||||||
def LICENSE() -> str:
|
def LICENSE() -> str:
|
||||||
return "GPL"
|
return "GPL"
|
||||||
|
|
||||||
def some_normal_function():
|
|
||||||
print("normal function")
|
|
||||||
|
|
||||||
# compiles and dumps object file in the same directory
|
b = BPF()
|
||||||
compile()
|
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
|
## 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
|
1. Create a virtual environment and activate it:
|
||||||
- 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`.
|
```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
|
## 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?
|
|
||||||
405
blazesym-example/Cargo.lock
generated
Normal file
405
blazesym-example/Cargo.lock
generated
Normal file
@ -0,0 +1,405 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstream"
|
||||||
|
version = "0.6.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"anstyle-parse",
|
||||||
|
"anstyle-query",
|
||||||
|
"anstyle-wincon",
|
||||||
|
"colorchoice",
|
||||||
|
"is_terminal_polyfill",
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-query"
|
||||||
|
version = "1.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-wincon"
|
||||||
|
version = "3.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"once_cell_polyfill",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blazesym"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ace0ab71bbe9a25cb82f6d0e513ae11aebd1a38787664475bb2ed5cbe2329736"
|
||||||
|
dependencies = [
|
||||||
|
"cpp_demangle",
|
||||||
|
"gimli",
|
||||||
|
"libc",
|
||||||
|
"memmap2",
|
||||||
|
"miniz_oxide",
|
||||||
|
"rustc-demangle",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.2.46"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36"
|
||||||
|
dependencies = [
|
||||||
|
"find-msvc-tools",
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.5.51"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
"clap_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.5.51"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "4.5.49"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpp_demangle"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0667304c32ea56cb4cd6d2d7c0cfe9a2f8041229db8c033af7f8d69492429def"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible-iterator"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "find-msvc-tools"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gimli"
|
||||||
|
version = "0.32.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
|
||||||
|
dependencies = [
|
||||||
|
"fallible-iterator",
|
||||||
|
"indexmap",
|
||||||
|
"stable_deref_trait",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libbpf-rs"
|
||||||
|
version = "0.24.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93edd9cd673087fa7518fd63ad6c87be2cd9b4e35034b1873f3e3258c018275b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"libbpf-sys",
|
||||||
|
"libc",
|
||||||
|
"vsprintf",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libbpf-sys"
|
||||||
|
version = "1.6.2+v1.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba0346fc595fa2c8e274903e8a0e3ed5e6a29183af167567f6289fd3b116881b"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"nix",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.177"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memmap2"
|
||||||
|
version = "0.9.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||||
|
dependencies = [
|
||||||
|
"adler2",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.30.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkg-config"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "plain"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.103"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blazesym-example"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"blazesym",
|
||||||
|
"clap",
|
||||||
|
"libbpf-rs",
|
||||||
|
"libc",
|
||||||
|
"plain",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-demangle"
|
||||||
|
version = "0.1.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simd-adler32"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stable_deref_trait"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.110"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vsprintf"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aec2f81b75ca063294776b4f7e8da71d1d5ae81c2b1b149c8d89969230265d63"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
14
blazesym-example/Cargo.toml
Normal file
14
blazesym-example/Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "blazesym-example"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
libbpf-rs = "0.24"
|
||||||
|
blazesym = "0.2.0-rc.4"
|
||||||
|
anyhow = "1.0"
|
||||||
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
|
libc = "0.2"
|
||||||
|
plain = "0.2"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
333
blazesym-example/src/main.rs
Normal file
333
blazesym-example/src/main.rs
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
// src/main.rs - Fixed imports and error handling
|
||||||
|
use std::mem;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use blazesym::symbolize::{CodeInfo, Input, Symbolized, Symbolizer};
|
||||||
|
use blazesym::symbolize::source::{Source, Kernel, Process};
|
||||||
|
use clap::Parser;
|
||||||
|
use libbpf_rs::{MapCore, ObjectBuilder, RingBufferBuilder}; // Added MapCore
|
||||||
|
|
||||||
|
// Match your Python struct exactly
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
struct ExecEvent {
|
||||||
|
pid: i64,
|
||||||
|
cpu: i32,
|
||||||
|
timestamp: i64,
|
||||||
|
comm: [u8; 16],
|
||||||
|
kstack_sz: i64,
|
||||||
|
ustack_sz: i64,
|
||||||
|
kstack: [u8; 128], // str(128) in Python
|
||||||
|
ustack: [u8; 128], // str(128) in Python
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl plain::Plain for ExecEvent {}
|
||||||
|
|
||||||
|
// Define perf_event constants (not in libc on all platforms)
|
||||||
|
const PERF_TYPE_HARDWARE: u32 = 0;
|
||||||
|
const PERF_TYPE_SOFTWARE: u32 = 1;
|
||||||
|
const PERF_COUNT_HW_CPU_CYCLES: u64 = 0;
|
||||||
|
const PERF_COUNT_SW_CPU_CLOCK: u64 = 0;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
struct PerfEventAttr {
|
||||||
|
type_: u32,
|
||||||
|
size: u32,
|
||||||
|
config: u64,
|
||||||
|
sample_period_or_freq: u64,
|
||||||
|
sample_type: u64,
|
||||||
|
read_format: u64,
|
||||||
|
flags: u64,
|
||||||
|
// ... rest can be zeroed
|
||||||
|
_padding: [u64; 64],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
struct Args {
|
||||||
|
/// Path to the BPF object file
|
||||||
|
#[arg(default_value = "stack_traces.o")]
|
||||||
|
object_file: PathBuf,
|
||||||
|
|
||||||
|
/// Sampling frequency
|
||||||
|
#[arg(short, long, default_value_t = 50)]
|
||||||
|
freq: u64,
|
||||||
|
|
||||||
|
/// Use software events
|
||||||
|
#[arg(long)]
|
||||||
|
sw_event: bool,
|
||||||
|
|
||||||
|
/// Verbose output
|
||||||
|
#[arg(short, long)]
|
||||||
|
verbose: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_perf_event(cpu: i32, freq: u64, sw_event: bool) -> Result<i32> {
|
||||||
|
let mut attr: PerfEventAttr = unsafe { mem::zeroed() };
|
||||||
|
|
||||||
|
attr.size = mem::size_of::<PerfEventAttr>() as u32;
|
||||||
|
attr.type_ = if sw_event {
|
||||||
|
PERF_TYPE_SOFTWARE
|
||||||
|
} else {
|
||||||
|
PERF_TYPE_HARDWARE
|
||||||
|
};
|
||||||
|
attr.config = if sw_event {
|
||||||
|
PERF_COUNT_SW_CPU_CLOCK
|
||||||
|
} else {
|
||||||
|
PERF_COUNT_HW_CPU_CYCLES
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use frequency-based sampling
|
||||||
|
attr.sample_period_or_freq = freq;
|
||||||
|
attr.flags = 1 << 10; // freq = 1, disabled = 1
|
||||||
|
|
||||||
|
let fd = unsafe {
|
||||||
|
libc::syscall(
|
||||||
|
libc::SYS_perf_event_open,
|
||||||
|
&attr as *const _,
|
||||||
|
-1, // pid = -1 (all processes)
|
||||||
|
cpu, // cpu
|
||||||
|
-1, // group_fd
|
||||||
|
0, // flags
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if fd < 0 {
|
||||||
|
Err(anyhow!("Failed to open perf event on CPU {}: {}", cpu,
|
||||||
|
std::io::Error::last_os_error()))
|
||||||
|
} else {
|
||||||
|
Ok(fd as i32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_stack_trace(
|
||||||
|
addrs: &[u64],
|
||||||
|
symbolizer: &Symbolizer,
|
||||||
|
pid: u32,
|
||||||
|
is_kernel: bool,
|
||||||
|
) {
|
||||||
|
if addrs.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let src = if is_kernel {
|
||||||
|
Source::Kernel(Kernel::default())
|
||||||
|
} else {
|
||||||
|
Source::Process(Process::new(pid.into()))
|
||||||
|
};
|
||||||
|
|
||||||
|
let syms = match symbolizer.symbolize(&src, Input::AbsAddr(addrs)) {
|
||||||
|
Ok(syms) => syms,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(" Failed to symbolize: {}", e);
|
||||||
|
for addr in addrs {
|
||||||
|
println!("0x{:016x}: <no-symbol>", addr);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (addr, sym) in addrs.iter().zip(syms.iter()) {
|
||||||
|
match sym {
|
||||||
|
Symbolized::Sym(sym_info) => {
|
||||||
|
print!("0x{:016x}: {} @ 0x{:x}+0x{:x}",
|
||||||
|
addr, sym_info.name, sym_info.addr, sym_info.offset);
|
||||||
|
|
||||||
|
if let Some(ref code_info) = sym_info.code_info {
|
||||||
|
print_code_info(code_info);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// Print inlined frames
|
||||||
|
for inlined in &sym_info.inlined {
|
||||||
|
print!(" {} (inlined)", inlined.name);
|
||||||
|
if let Some(ref code_info) = inlined.code_info {
|
||||||
|
print_code_info(code_info);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Symbolized::Unknown(..) => {
|
||||||
|
println!("0x{:016x}: <no-symbol>", addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_code_info(code_info: &CodeInfo) {
|
||||||
|
let path = code_info.to_path();
|
||||||
|
let path_str = path.display();
|
||||||
|
|
||||||
|
match (code_info.line, code_info.column) {
|
||||||
|
(Some(line), Some(col)) => print!(" {}:{}:{}", path_str, line, col),
|
||||||
|
(Some(line), None) => print!(" {}:{}", path_str, line),
|
||||||
|
(None, _) => print!(" {}", path_str),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_event(symbolizer: &Symbolizer, data: &[u8]) -> i32 {
|
||||||
|
let event = plain::from_bytes::<ExecEvent>(data).expect("Invalid event data");
|
||||||
|
|
||||||
|
// Extract comm string
|
||||||
|
let comm = std::str::from_utf8(&event.comm)
|
||||||
|
.unwrap_or("<unknown>")
|
||||||
|
.trim_end_matches('\0');
|
||||||
|
|
||||||
|
println!("[{:.9}] COMM: {} (pid={}) @ CPU {}",
|
||||||
|
event.timestamp as f64 / 1_000_000_000.0,
|
||||||
|
comm,
|
||||||
|
event.pid,
|
||||||
|
event.cpu);
|
||||||
|
|
||||||
|
// Handle kernel stack
|
||||||
|
if event.kstack_sz > 0 {
|
||||||
|
println!("Kernel:");
|
||||||
|
let num_frames = (event.kstack_sz / 8) as usize;
|
||||||
|
let kstack_u64 = unsafe {
|
||||||
|
std::slice::from_raw_parts(
|
||||||
|
event.kstack.as_ptr() as *const u64,
|
||||||
|
num_frames.min(16),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter out zero addresses
|
||||||
|
let kstack: Vec<u64> = kstack_u64.iter()
|
||||||
|
.copied()
|
||||||
|
.take_while(|&addr| addr != 0)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
print_stack_trace(&kstack, symbolizer, 0, true);
|
||||||
|
} else {
|
||||||
|
println!("No Kernel Stack");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle user stack
|
||||||
|
if event.ustack_sz > 0 {
|
||||||
|
println!("Userspace:");
|
||||||
|
let num_frames = (event.ustack_sz / 8) as usize;
|
||||||
|
let ustack_u64 = unsafe {
|
||||||
|
std::slice::from_raw_parts(
|
||||||
|
event.ustack.as_ptr() as *const u64,
|
||||||
|
num_frames.min(16),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter out zero addresses
|
||||||
|
let ustack: Vec<u64> = ustack_u64.iter()
|
||||||
|
.copied()
|
||||||
|
.take_while(|&addr| addr != 0)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
print_stack_trace(&ustack, symbolizer, event.pid as u32, false);
|
||||||
|
} else {
|
||||||
|
println!("No Userspace Stack");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
if !args.object_file.exists() {
|
||||||
|
return Err(anyhow!("Object file not found: {:?}", args.object_file));
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Loading BPF object: {:?}", args.object_file);
|
||||||
|
|
||||||
|
// Load BPF object
|
||||||
|
let mut obj_builder = ObjectBuilder::default();
|
||||||
|
obj_builder.debug(args.verbose);
|
||||||
|
|
||||||
|
let open_obj = obj_builder
|
||||||
|
.open_file(&args.object_file)
|
||||||
|
.context("Failed to open BPF object")?;
|
||||||
|
|
||||||
|
let mut obj = open_obj.load().context("Failed to load BPF object")?;
|
||||||
|
|
||||||
|
println!("✓ BPF object loaded");
|
||||||
|
|
||||||
|
// Find the program
|
||||||
|
let prog = obj
|
||||||
|
.progs_mut()
|
||||||
|
.find(|p| p.name() == "trace_exec_enter")
|
||||||
|
.ok_or_else(|| anyhow!("Program 'trace_exec_enter' not found"))?;
|
||||||
|
|
||||||
|
println!("✓ Found program: trace_exec_enter");
|
||||||
|
|
||||||
|
// Find the map
|
||||||
|
let map = obj
|
||||||
|
.maps()
|
||||||
|
.find(|m| m.name() == "exec_events")
|
||||||
|
.ok_or_else(|| anyhow!("Map 'exec_events' not found"))?;
|
||||||
|
|
||||||
|
println!("✓ Found map: exec_events");
|
||||||
|
|
||||||
|
// Get number of CPUs
|
||||||
|
let num_cpus = libbpf_rs::num_possible_cpus()?;
|
||||||
|
println!("✓ Detected {} CPUs\n", num_cpus);
|
||||||
|
|
||||||
|
// Open perf events and attach BPF program
|
||||||
|
println!("Setting up perf events...");
|
||||||
|
let mut links = Vec::new();
|
||||||
|
|
||||||
|
for cpu in 0..num_cpus {
|
||||||
|
match open_perf_event(cpu as i32, args.freq, args.sw_event) {
|
||||||
|
Ok(perf_fd) => {
|
||||||
|
match prog.attach_perf_event(perf_fd) {
|
||||||
|
Ok(link) => {
|
||||||
|
links.push(link);
|
||||||
|
if args.verbose {
|
||||||
|
println!(" ✓ Attached to CPU {}", cpu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(" ✗ Failed to attach to CPU {}: {}", cpu, e);
|
||||||
|
unsafe { libc::close(perf_fd); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if args.verbose {
|
||||||
|
eprintln!(" ✗ Failed to open perf event on CPU {}: {}", cpu, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("✓ Attached to {} CPUs\n", links.len());
|
||||||
|
|
||||||
|
if links.is_empty() {
|
||||||
|
return Err(anyhow!("Failed to attach to any CPU"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize symbolizer
|
||||||
|
let symbolizer = Symbolizer::new();
|
||||||
|
|
||||||
|
// Set up ring buffer
|
||||||
|
let mut builder = RingBufferBuilder::new();
|
||||||
|
|
||||||
|
builder.add(&map, move |data: &[u8]| -> i32 {
|
||||||
|
handle_event(&symbolizer, data)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let ringbuf = builder.build()?;
|
||||||
|
|
||||||
|
println!("========================================");
|
||||||
|
println!("Profiling started. Press Ctrl+C to stop.");
|
||||||
|
println!("========================================\n");
|
||||||
|
|
||||||
|
// Poll for events - just keep polling until error
|
||||||
|
loop {
|
||||||
|
if let Err(e) = ringbuf.poll(Duration::from_millis(100)) {
|
||||||
|
// Any error breaks the loop (including Ctrl+C)
|
||||||
|
eprintln!("\nStopping: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Done.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
49
blazesym-example/stack_traces.py
Normal file
49
blazesym-example/stack_traces.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# tests/passing_tests/ringbuf_advanced.py
|
||||||
|
from pythonbpf import bpf, map, section, bpfglobal, struct, compile
|
||||||
|
from pythonbpf.maps import RingBuffer
|
||||||
|
from pythonbpf.helper import ktime, pid, smp_processor_id, comm, get_stack
|
||||||
|
from ctypes import c_void_p, c_int32, c_int64
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class exec_event:
|
||||||
|
pid: c_int64
|
||||||
|
cpu: c_int32
|
||||||
|
timestamp: c_int64
|
||||||
|
comm: str(16) # type: ignore [valid-type]
|
||||||
|
kstack_sz: c_int64
|
||||||
|
ustack_sz: c_int64
|
||||||
|
kstack: str(128) # type: ignore [valid-type]
|
||||||
|
ustack: str(128) # type: ignore [valid-type]
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def exec_events() -> RingBuffer:
|
||||||
|
return RingBuffer(max_entries=1048576)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("perf_event")
|
||||||
|
def trace_exec_enter(ctx: c_void_p) -> c_int64:
|
||||||
|
evt = exec_event()
|
||||||
|
evt.pid = pid()
|
||||||
|
evt.cpu = smp_processor_id()
|
||||||
|
evt.timestamp = ktime()
|
||||||
|
comm(evt.comm)
|
||||||
|
evt.kstack_sz = get_stack(evt.kstack)
|
||||||
|
evt.ustack_sz = get_stack(evt.ustack, 256)
|
||||||
|
exec_events.output(evt)
|
||||||
|
print(f"Submitted exec_event for pid: {evt.pid}, cpu: {evt.cpu}")
|
||||||
|
return 0 # type: ignore [return-value]
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
|
||||||
|
compile(logging.INFO)
|
||||||
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()
|
|
||||||
File diff suppressed because one or more lines are too long
@ -1,43 +0,0 @@
|
|||||||
from pythonbpf import bpf, map, section, bpfglobal, compile
|
|
||||||
from pythonbpf.helpers 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
|
|
||||||
# 4. Start a Python repl and `import os` and then keep entering `os.sync()` to see reponses.
|
|
||||||
|
|
||||||
@bpf
|
|
||||||
@map
|
|
||||||
def last() -> HashMap:
|
|
||||||
return HashMap(key=c_uint64, value=c_uint64, max_entries=3)
|
|
||||||
|
|
||||||
|
|
||||||
@bpf
|
|
||||||
@section("tracepoint/syscalls/sys_enter_sync")
|
|
||||||
def do_trace(ctx: c_void_p) -> c_int64:
|
|
||||||
key = 0
|
|
||||||
tsp = last().lookup(key)
|
|
||||||
if tsp:
|
|
||||||
kt = ktime()
|
|
||||||
delta = (kt - tsp)
|
|
||||||
if delta < 1000000000:
|
|
||||||
time_ms = (delta // 1000000)
|
|
||||||
print(f"sync called within last second, last {time_ms} ms ago")
|
|
||||||
last().delete(key)
|
|
||||||
else:
|
|
||||||
kt = ktime()
|
|
||||||
last().update(key, kt)
|
|
||||||
return c_int64(0)
|
|
||||||
|
|
||||||
|
|
||||||
@bpf
|
|
||||||
@bpfglobal
|
|
||||||
def LICENSE() -> str:
|
|
||||||
return "GPL"
|
|
||||||
|
|
||||||
|
|
||||||
compile()
|
|
||||||
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Minimal makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line, and also
|
||||||
|
# from the environment for the first two.
|
||||||
|
SPHINXOPTS ?=
|
||||||
|
SPHINXBUILD ?= sphinx-build
|
||||||
|
SOURCEDIR = .
|
||||||
|
BUILDDIR = _build
|
||||||
|
|
||||||
|
# Put it first so that "make" without argument is like "make help".
|
||||||
|
help:
|
||||||
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
|
.PHONY: help Makefile
|
||||||
|
|
||||||
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
|
%: Makefile
|
||||||
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
52
docs/README.md
Normal file
52
docs/README.md
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# PythonBPF Documentation
|
||||||
|
|
||||||
|
This directory contains the Sphinx documentation for PythonBPF.
|
||||||
|
|
||||||
|
## Building the Documentation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
Install the documentation dependencies:
|
||||||
|
|
||||||
|
**Using uv (recommended):**
|
||||||
|
```bash
|
||||||
|
uv pip install -r requirements.txt
|
||||||
|
# Or install the optional docs dependencies
|
||||||
|
uv pip install pythonbpf[docs]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Using pip:**
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
# Or install the optional docs dependencies
|
||||||
|
pip install pythonbpf[docs]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build HTML Documentation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make html
|
||||||
|
```
|
||||||
|
|
||||||
|
The generated documentation will be in `_build/html/`. Open `_build/html/index.html` in a browser to view.
|
||||||
|
|
||||||
|
### Other Build Formats
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make latexpdf # Build PDF documentation
|
||||||
|
make epub # Build ePub format
|
||||||
|
make clean # Clean build artifacts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation Structure
|
||||||
|
|
||||||
|
- `index.md` - Main landing page
|
||||||
|
- `getting-started/` - Installation and quick start guides
|
||||||
|
- `user-guide/` - Comprehensive user documentation
|
||||||
|
- `api/` - API reference documentation
|
||||||
|
- `conf.py` - Sphinx configuration
|
||||||
|
- `_static/` - Static files (images, CSS, etc.)
|
||||||
|
|
||||||
|
## Writing Documentation
|
||||||
|
|
||||||
|
Documentation is written in Markdown using [MyST-Parser](https://myst-parser.readthedocs.io/). See the existing files for examples.
|
||||||
0
docs/_static/.gitkeep
vendored
Normal file
0
docs/_static/.gitkeep
vendored
Normal file
471
docs/api/index.md
Normal file
471
docs/api/index.md
Normal file
@ -0,0 +1,471 @@
|
|||||||
|
# API Reference
|
||||||
|
|
||||||
|
This section provides detailed API documentation for all PythonBPF modules, classes, and functions.
|
||||||
|
|
||||||
|
## Module Overview
|
||||||
|
|
||||||
|
PythonBPF is organized into several modules:
|
||||||
|
|
||||||
|
* `pythonbpf` - Main module with decorators and compilation functions
|
||||||
|
* `pythonbpf.maps` - BPF map types
|
||||||
|
* `pythonbpf.helper` - BPF helper functions
|
||||||
|
* `pythonbpf.structs` - Struct type handling
|
||||||
|
* `pythonbpf.codegen` - Code generation and compilation
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
The main `pythonbpf` module exports the following public API:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import (
|
||||||
|
# Decorators
|
||||||
|
bpf,
|
||||||
|
map,
|
||||||
|
section,
|
||||||
|
bpfglobal,
|
||||||
|
struct,
|
||||||
|
|
||||||
|
# Compilation
|
||||||
|
compile_to_ir,
|
||||||
|
compile,
|
||||||
|
BPF,
|
||||||
|
|
||||||
|
# Utilities
|
||||||
|
trace_pipe,
|
||||||
|
trace_fields,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Decorators
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: pythonbpf.decorators
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
```
|
||||||
|
|
||||||
|
### bpf
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf
|
||||||
|
def my_function():
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
Decorator to mark a function or class for BPF compilation. Any function or class decorated with `@bpf` will be processed by the PythonBPF compiler.
|
||||||
|
|
||||||
|
**See also:** {doc}`../user-guide/decorators`
|
||||||
|
|
||||||
|
### map
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def my_map() -> HashMap:
|
||||||
|
return HashMap(key=c_uint32, value=c_uint64, max_entries=1024)
|
||||||
|
```
|
||||||
|
|
||||||
|
Decorator to mark a function as a BPF map definition. The function must return a map type.
|
||||||
|
|
||||||
|
**See also:** {doc}`../user-guide/maps`
|
||||||
|
|
||||||
|
### section
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_open")
|
||||||
|
def trace_open(ctx: c_void_p) -> c_int64:
|
||||||
|
return c_int64(0)
|
||||||
|
```
|
||||||
|
|
||||||
|
Decorator to specify which kernel hook to attach the BPF program to.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
* `name` (str) - The section name (e.g., "tracepoint/...", "kprobe/...", "xdp")
|
||||||
|
|
||||||
|
**See also:** {doc}`../user-guide/decorators`
|
||||||
|
|
||||||
|
### bpfglobal
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
```
|
||||||
|
|
||||||
|
Decorator to mark a function as a BPF global variable definition.
|
||||||
|
|
||||||
|
**See also:** {doc}`../user-guide/decorators`
|
||||||
|
|
||||||
|
### struct
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class Event:
|
||||||
|
timestamp: c_uint64
|
||||||
|
pid: c_uint32
|
||||||
|
```
|
||||||
|
|
||||||
|
Decorator to mark a class as a BPF struct definition.
|
||||||
|
|
||||||
|
**See also:** {doc}`../user-guide/structs`
|
||||||
|
|
||||||
|
## Compilation Functions
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: pythonbpf.codegen
|
||||||
|
:members: compile_to_ir, compile, BPF
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
```
|
||||||
|
|
||||||
|
### compile_to_ir()
|
||||||
|
|
||||||
|
```python
|
||||||
|
def compile_to_ir(
|
||||||
|
filename: str,
|
||||||
|
output: str,
|
||||||
|
loglevel=logging.WARNING
|
||||||
|
) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
Compile Python source to LLVM Intermediate Representation.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
* `filename` (str) - Path to the Python source file
|
||||||
|
* `output` (str) - Path for the output LLVM IR file (.ll)
|
||||||
|
* `loglevel` - Logging level (default: logging.WARNING)
|
||||||
|
|
||||||
|
**See also:** {doc}`../user-guide/compilation`
|
||||||
|
|
||||||
|
### compile()
|
||||||
|
|
||||||
|
```python
|
||||||
|
def compile(
|
||||||
|
filename: str = None,
|
||||||
|
output: str = None,
|
||||||
|
loglevel=logging.WARNING
|
||||||
|
) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
Compile Python source to BPF object file.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
* `filename` (str, optional) - Path to the Python source file (default: calling file)
|
||||||
|
* `output` (str, optional) - Path for the output object file (default: same name with .o extension)
|
||||||
|
* `loglevel` - Logging level (default: logging.WARNING)
|
||||||
|
|
||||||
|
**See also:** {doc}`../user-guide/compilation`
|
||||||
|
|
||||||
|
### BPF
|
||||||
|
|
||||||
|
```python
|
||||||
|
class BPF:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
filename: str = None,
|
||||||
|
loglevel=logging.WARNING
|
||||||
|
)
|
||||||
|
|
||||||
|
def load(self) -> BpfObject
|
||||||
|
def attach_all(self) -> None
|
||||||
|
def load_and_attach(self) -> BpfObject
|
||||||
|
```
|
||||||
|
|
||||||
|
High-level interface to compile, load, and attach BPF programs.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
* `filename` (str, optional) - Path to Python source file (default: calling file)
|
||||||
|
* `loglevel` - Logging level (default: logging.WARNING)
|
||||||
|
|
||||||
|
**Methods:**
|
||||||
|
* `load()` - Load the compiled BPF program into the kernel
|
||||||
|
* `attach_all()` - Attach all BPF programs to their hooks
|
||||||
|
* `load_and_attach()` - Convenience method that loads and attaches
|
||||||
|
|
||||||
|
**See also:** {doc}`../user-guide/compilation`
|
||||||
|
|
||||||
|
## Utilities
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: pythonbpf.utils
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
```
|
||||||
|
|
||||||
|
### trace_pipe()
|
||||||
|
|
||||||
|
```python
|
||||||
|
def trace_pipe() -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
Read and display output from the kernel trace pipe.
|
||||||
|
|
||||||
|
Blocks until interrupted with Ctrl+C. Displays BPF program output from `print()` statements.
|
||||||
|
|
||||||
|
**See also:** {doc}`../user-guide/helpers`
|
||||||
|
|
||||||
|
### trace_fields()
|
||||||
|
|
||||||
|
```python
|
||||||
|
def trace_fields() -> tuple
|
||||||
|
```
|
||||||
|
|
||||||
|
Parse one line from the trace pipe into structured fields.
|
||||||
|
|
||||||
|
**Returns:** Tuple of `(task, pid, cpu, flags, timestamp, message)`
|
||||||
|
* `task` (str) - Task/process name
|
||||||
|
* `pid` (int) - Process ID
|
||||||
|
* `cpu` (int) - CPU number
|
||||||
|
* `flags` (bytes) - Trace flags
|
||||||
|
* `timestamp` (float) - Timestamp in seconds
|
||||||
|
* `message` (str) - The trace message
|
||||||
|
|
||||||
|
**See also:** {doc}`../user-guide/helpers`
|
||||||
|
|
||||||
|
## Map Types
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: pythonbpf.maps.maps
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
```
|
||||||
|
|
||||||
|
### HashMap
|
||||||
|
|
||||||
|
```python
|
||||||
|
class HashMap:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
max_entries: int
|
||||||
|
)
|
||||||
|
|
||||||
|
def lookup(self, key)
|
||||||
|
def update(self, key, value, flags=None)
|
||||||
|
def delete(self, key)
|
||||||
|
```
|
||||||
|
|
||||||
|
Hash map for efficient key-value storage.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
* `key` - The type of the key (ctypes type or struct)
|
||||||
|
* `value` - The type of the value (ctypes type or struct)
|
||||||
|
* `max_entries` (int) - Maximum number of entries
|
||||||
|
|
||||||
|
**Methods:**
|
||||||
|
* `lookup(key)` - Look up a value by key
|
||||||
|
* `update(key, value, flags=None)` - Update or insert a key-value pair
|
||||||
|
* `delete(key)` - Remove an entry from the map
|
||||||
|
|
||||||
|
**See also:** {doc}`../user-guide/maps`
|
||||||
|
|
||||||
|
### PerfEventArray
|
||||||
|
|
||||||
|
```python
|
||||||
|
class PerfEventArray:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
key_size,
|
||||||
|
value_size
|
||||||
|
)
|
||||||
|
|
||||||
|
def output(self, data)
|
||||||
|
```
|
||||||
|
|
||||||
|
Perf event array for sending data to userspace.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
* `key_size` - Type for the key
|
||||||
|
* `value_size` - Type for the value
|
||||||
|
|
||||||
|
**Methods:**
|
||||||
|
* `output(data)` - Send data to userspace
|
||||||
|
|
||||||
|
**See also:** {doc}`../user-guide/maps`
|
||||||
|
|
||||||
|
### RingBuffer
|
||||||
|
|
||||||
|
```python
|
||||||
|
class RingBuffer:
|
||||||
|
def __init__(self, max_entries: int)
|
||||||
|
|
||||||
|
def output(self, data, flags=0)
|
||||||
|
def reserve(self, size: int)
|
||||||
|
def submit(self, data, flags=0)
|
||||||
|
def discard(self, data, flags=0)
|
||||||
|
```
|
||||||
|
|
||||||
|
Ring buffer for efficient event delivery.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
* `max_entries` (int) - Maximum size in bytes (must be power of 2)
|
||||||
|
|
||||||
|
**Methods:**
|
||||||
|
* `output(data, flags=0)` - Send data to the ring buffer
|
||||||
|
* `reserve(size)` - Reserve space in the buffer
|
||||||
|
* `submit(data, flags=0)` - Submit previously reserved space
|
||||||
|
* `discard(data, flags=0)` - Discard previously reserved space
|
||||||
|
|
||||||
|
**See also:** {doc}`../user-guide/maps`
|
||||||
|
|
||||||
|
## Helper Functions
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: pythonbpf.helper.helpers
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
```
|
||||||
|
|
||||||
|
### Process Information
|
||||||
|
|
||||||
|
* `pid()` - Get current process ID
|
||||||
|
* `comm(buf)` - Get current process command name (requires buffer parameter)
|
||||||
|
* `uid()` - Get current user ID
|
||||||
|
|
||||||
|
### Time
|
||||||
|
|
||||||
|
* `ktime()` - Get current kernel time in nanoseconds
|
||||||
|
|
||||||
|
### CPU
|
||||||
|
|
||||||
|
* `smp_processor_id()` - Get current CPU ID
|
||||||
|
|
||||||
|
### Memory
|
||||||
|
|
||||||
|
* `probe_read(dst, size, src)` - Safely read kernel memory
|
||||||
|
* `probe_read_str(dst, src)` - Safely read string from kernel memory
|
||||||
|
* `deref(ptr)` - Dereference a pointer
|
||||||
|
|
||||||
|
### Random
|
||||||
|
|
||||||
|
* `random()` - Get pseudo-random number
|
||||||
|
|
||||||
|
**See also:** {doc}`../user-guide/helpers`
|
||||||
|
|
||||||
|
## Type System
|
||||||
|
|
||||||
|
PythonBPF uses Python's `ctypes` module for type definitions:
|
||||||
|
|
||||||
|
### Integer Types
|
||||||
|
|
||||||
|
* `c_int8`, `c_int16`, `c_int32`, `c_int64` - Signed integers
|
||||||
|
* `c_uint8`, `c_uint16`, `c_uint32`, `c_uint64` - Unsigned integers
|
||||||
|
|
||||||
|
### Other Types
|
||||||
|
|
||||||
|
* `c_char`, `c_bool` - Characters and booleans
|
||||||
|
* `c_void_p` - Void pointers
|
||||||
|
* `str(N)` - Fixed-length strings
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, section, bpfglobal, BPF, trace_pipe
|
||||||
|
from ctypes import c_void_p, c_int64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_execve")
|
||||||
|
def hello(ctx: c_void_p) -> c_int64:
|
||||||
|
print("Hello, World!")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
b = BPF()
|
||||||
|
b.load_and_attach()
|
||||||
|
trace_pipe()
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Maps
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, map, section, bpfglobal, BPF
|
||||||
|
from pythonbpf.maps import HashMap
|
||||||
|
from pythonbpf.helper import pid
|
||||||
|
from ctypes import c_void_p, c_int64, c_uint32, c_uint64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def counters() -> HashMap:
|
||||||
|
return HashMap(key=c_uint32, value=c_uint64, max_entries=256)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_clone")
|
||||||
|
def count_clones(ctx: c_void_p) -> c_int64:
|
||||||
|
process_id = pid()
|
||||||
|
count = counters.lookup(process_id)
|
||||||
|
|
||||||
|
if count:
|
||||||
|
counters.update(process_id, count + 1)
|
||||||
|
else:
|
||||||
|
counters.update(process_id, 1)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
b = BPF()
|
||||||
|
b.load_and_attach()
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Structs
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, struct, map, section, bpfglobal, BPF
|
||||||
|
from pythonbpf.maps import RingBuffer
|
||||||
|
from pythonbpf.helper import pid, ktime, comm
|
||||||
|
from ctypes import c_void_p, c_int64, c_uint32, c_uint64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class Event:
|
||||||
|
timestamp: c_uint64
|
||||||
|
pid: c_uint32
|
||||||
|
comm: str(16)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def events() -> RingBuffer:
|
||||||
|
return RingBuffer(max_entries=4096)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_execve")
|
||||||
|
def track_exec(ctx: c_void_p) -> c_int64:
|
||||||
|
event = Event()
|
||||||
|
event.timestamp = ktime()
|
||||||
|
event.pid = pid()
|
||||||
|
comm(event.comm)
|
||||||
|
|
||||||
|
events.output(event)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
b = BPF()
|
||||||
|
b.load_and_attach()
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* {doc}`../user-guide/index` - Comprehensive user guide
|
||||||
|
* {doc}`../getting-started/quickstart` - Quick start tutorial
|
||||||
|
* [GitHub Repository](https://github.com/pythonbpf/Python-BPF) - Source code and examples
|
||||||
105
docs/conf.py
Normal file
105
docs/conf.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
# Configuration file for the Sphinx documentation builder.
|
||||||
|
#
|
||||||
|
# For the full list of built-in configuration values, see the documentation:
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Add the parent directory to the path so we can import pythonbpf
|
||||||
|
sys.path.insert(0, os.path.abspath(".."))
|
||||||
|
|
||||||
|
# -- Project information -----------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||||
|
|
||||||
|
project = "PythonBPF"
|
||||||
|
copyright = "2026, Pragyansh Chaturvedi, Varun Mallya"
|
||||||
|
author = "Pragyansh Chaturvedi, Varun Mallya"
|
||||||
|
release = "0.1.8"
|
||||||
|
version = "0.1.8"
|
||||||
|
|
||||||
|
# -- General configuration ---------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||||
|
|
||||||
|
extensions = [
|
||||||
|
"myst_parser",
|
||||||
|
"sphinx.ext.autodoc",
|
||||||
|
"sphinx.ext.napoleon",
|
||||||
|
"sphinx.ext.viewcode",
|
||||||
|
"sphinx.ext.intersphinx",
|
||||||
|
"sphinx_copybutton",
|
||||||
|
]
|
||||||
|
|
||||||
|
# MyST-Parser configuration
|
||||||
|
myst_enable_extensions = [
|
||||||
|
"colon_fence",
|
||||||
|
"deflist",
|
||||||
|
"fieldlist",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Napoleon settings for Google/NumPy style docstrings
|
||||||
|
napoleon_google_docstring = True
|
||||||
|
napoleon_numpy_docstring = True
|
||||||
|
napoleon_include_init_with_doc = True
|
||||||
|
napoleon_include_private_with_doc = False
|
||||||
|
napoleon_include_special_with_doc = True
|
||||||
|
napoleon_use_admonition_for_examples = True
|
||||||
|
napoleon_use_admonition_for_notes = True
|
||||||
|
napoleon_use_admonition_for_references = False
|
||||||
|
napoleon_use_ivar = False
|
||||||
|
napoleon_use_param = True
|
||||||
|
napoleon_use_rtype = True
|
||||||
|
napoleon_type_aliases = None
|
||||||
|
|
||||||
|
# Intersphinx mapping
|
||||||
|
intersphinx_mapping = {
|
||||||
|
"python": ("https://docs.python.org/3", None),
|
||||||
|
"llvmlite": ("https://llvmlite.readthedocs.io/en/latest/", None),
|
||||||
|
}
|
||||||
|
|
||||||
|
templates_path = ["_templates"]
|
||||||
|
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||||
|
|
||||||
|
# Source file suffixes
|
||||||
|
source_suffix = {
|
||||||
|
".rst": "restructuredtext",
|
||||||
|
".md": "markdown",
|
||||||
|
}
|
||||||
|
|
||||||
|
# The master toctree document
|
||||||
|
master_doc = "index"
|
||||||
|
|
||||||
|
# -- Options for HTML output -------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||||
|
|
||||||
|
html_theme = "sphinx_rtd_theme"
|
||||||
|
html_static_path = ["_static"]
|
||||||
|
|
||||||
|
# Theme options
|
||||||
|
html_theme_options = {
|
||||||
|
"logo_only": False,
|
||||||
|
"display_version": True,
|
||||||
|
"prev_next_buttons_location": "bottom",
|
||||||
|
"style_external_links": False,
|
||||||
|
"vcs_pageview_mode": "",
|
||||||
|
# Toc options
|
||||||
|
"collapse_navigation": False,
|
||||||
|
"sticky_navigation": True,
|
||||||
|
"navigation_depth": 4,
|
||||||
|
"includehidden": True,
|
||||||
|
"titles_only": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- Options for autodoc -----------------------------------------------------
|
||||||
|
|
||||||
|
autodoc_default_options = {
|
||||||
|
"members": True,
|
||||||
|
"member-order": "bysource",
|
||||||
|
"special-members": "__init__",
|
||||||
|
"undoc-members": True,
|
||||||
|
"exclude-members": "__weakref__",
|
||||||
|
}
|
||||||
|
|
||||||
|
autodoc_typehints = "description"
|
||||||
|
|
||||||
|
exclude_patterns = ["README.md"]
|
||||||
35
docs/getting-started/index.md
Normal file
35
docs/getting-started/index.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Getting Started
|
||||||
|
|
||||||
|
Welcome to PythonBPF! This section will help you get started with writing eBPF programs in Python.
|
||||||
|
|
||||||
|
## What You'll Learn
|
||||||
|
|
||||||
|
In this section, you'll learn how to:
|
||||||
|
|
||||||
|
1. **Install PythonBPF** - Set up your development environment with all necessary dependencies
|
||||||
|
2. **Write Your First Program** - Create a simple BPF program to understand the basics
|
||||||
|
3. **Understand Core Concepts** - Learn about decorators, compilation, and program structure
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before you begin, make sure you have:
|
||||||
|
|
||||||
|
* A Linux system (eBPF requires Linux kernel 4.15+)
|
||||||
|
* Python 3.10 or higher
|
||||||
|
* Root or sudo access (required for loading BPF programs)
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
After completing the getting started guide, you can:
|
||||||
|
|
||||||
|
* Explore the {doc}`../user-guide/index` for detailed information on features
|
||||||
|
* Check out the {doc}`../api/index`
|
||||||
|
* Browse the [examples directory](https://github.com/pythonbpf/Python-BPF/tree/master/examples) and the [BCC examples directory](https://github.com/pythonbpf/Python-BPF/tree/master/BCC-Examples)
|
||||||
|
|
||||||
|
## Need Help?
|
||||||
|
|
||||||
|
If you encounter any issues:
|
||||||
|
|
||||||
|
* Check the [GitHub Issues](https://github.com/pythonbpf/Python-BPF/issues) for known problems
|
||||||
|
* Review the [README](https://github.com/pythonbpf/Python-BPF/blob/master/README.md) for additional information
|
||||||
|
* Reach out to the maintainers: [@r41k0u](https://github.com/r41k0u) and [@varun-r-mallya](https://github.com/varun-r-mallya)
|
||||||
182
docs/getting-started/installation.md
Normal file
182
docs/getting-started/installation.md
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
# Installation
|
||||||
|
|
||||||
|
This guide will walk you through installing PythonBPF and its dependencies.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
### System Requirements
|
||||||
|
|
||||||
|
PythonBPF requires:
|
||||||
|
|
||||||
|
* **Linux** - eBPF is a Linux kernel feature (kernel 4.15 or higher recommended)
|
||||||
|
* **Python 3.10+** - Python 3.10 or higher is required
|
||||||
|
* **Root/sudo access** - Loading BPF programs into the kernel requires elevated privileges
|
||||||
|
|
||||||
|
### Required System Packages
|
||||||
|
|
||||||
|
Before installing PythonBPF, you need to install the following system packages:
|
||||||
|
|
||||||
|
#### On Ubuntu/Debian:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y bpftool clang llvm
|
||||||
|
```
|
||||||
|
|
||||||
|
#### On Fedora/RHEL/CentOS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dnf install -y bpftool clang llvm
|
||||||
|
```
|
||||||
|
|
||||||
|
#### On Arch Linux:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo pacman -S bpf clang llvm
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installing PythonBPF
|
||||||
|
|
||||||
|
### From PyPI (Recommended)
|
||||||
|
|
||||||
|
The easiest way to install PythonBPF is using uv or pip:
|
||||||
|
|
||||||
|
**Using uv (recommended):**
|
||||||
|
```bash
|
||||||
|
uv pip install pythonbpf pylibbpf
|
||||||
|
```
|
||||||
|
|
||||||
|
**Using pip:**
|
||||||
|
```bash
|
||||||
|
pip install pythonbpf pylibbpf
|
||||||
|
```
|
||||||
|
|
||||||
|
This will install:
|
||||||
|
* `pythonbpf` - The main package for writing and compiling BPF programs
|
||||||
|
* `pylibbpf` - Python bindings for libbpf, used to load and attach BPF programs
|
||||||
|
|
||||||
|
### Development Installation
|
||||||
|
|
||||||
|
If you want to contribute to PythonBPF or work with the latest development version:
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/pythonbpf/Python-BPF.git
|
||||||
|
cd Python-BPF
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create and activate a virtual environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Install in development mode:
|
||||||
|
|
||||||
|
**Using uv (recommended):**
|
||||||
|
```bash
|
||||||
|
uv pip install -e .
|
||||||
|
uv pip install pylibbpf
|
||||||
|
```
|
||||||
|
|
||||||
|
**Using pip:**
|
||||||
|
```bash
|
||||||
|
pip install -e .
|
||||||
|
pip install pylibbpf
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Install development dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Installing Documentation Dependencies
|
||||||
|
|
||||||
|
If you want to build the documentation locally:
|
||||||
|
|
||||||
|
**Using uv (recommended):**
|
||||||
|
```bash
|
||||||
|
uv pip install pythonbpf[docs]
|
||||||
|
# Or from the repository root:
|
||||||
|
uv pip install -e .[docs]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Using pip:**
|
||||||
|
```bash
|
||||||
|
pip install pythonbpf[docs]
|
||||||
|
# Or from the repository root:
|
||||||
|
pip install -e .[docs]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generating vmlinux.py
|
||||||
|
|
||||||
|
`vmlinux.py` contains the running kernel's data structures and is analogous to `vmlinux.h` included in eBPF programs written in C. Some examples require access to it. To use these features, you need to generate a `vmlinux.py` file:
|
||||||
|
|
||||||
|
1. Install additional dependencies:
|
||||||
|
|
||||||
|
**Using uv (recommended):**
|
||||||
|
```bash
|
||||||
|
uv pip install ctypeslib2
|
||||||
|
```
|
||||||
|
|
||||||
|
**Using pip:**
|
||||||
|
```bash
|
||||||
|
pip install ctypeslib2
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Generate the vmlinux.py file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo tools/vmlinux-gen.py
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Copy the generated file to your working directory or the examples directory as needed.
|
||||||
|
|
||||||
|
```{warning}
|
||||||
|
The `vmlinux.py` file is kernel-specific. If you upgrade your kernel, you may need to regenerate this file.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verifying Installation
|
||||||
|
|
||||||
|
To verify that PythonBPF is installed correctly, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -c "import pythonbpf; print(pythonbpf.__all__)"
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see output similar to:
|
||||||
|
|
||||||
|
```
|
||||||
|
['bpf', 'map', 'section', 'bpfglobal', 'struct', 'compile_to_ir', 'compile', 'BPF', 'trace_pipe', 'trace_fields']
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Permission Errors
|
||||||
|
|
||||||
|
If you encounter permission errors when running BPF programs:
|
||||||
|
|
||||||
|
* Make sure you're running with `sudo` or as root
|
||||||
|
* Check that `/sys/kernel/tracing/` is accessible
|
||||||
|
|
||||||
|
### LLVM/Clang Not Found
|
||||||
|
|
||||||
|
If you get errors about `llc` or `clang` not being found:
|
||||||
|
|
||||||
|
* Verify they're installed: `which llc` and `which clang`
|
||||||
|
* Check your PATH environment variable includes the LLVM bin directory
|
||||||
|
|
||||||
|
### Import Errors
|
||||||
|
|
||||||
|
If Python can't find the `pythonbpf` module:
|
||||||
|
|
||||||
|
* Make sure you've activated your virtual environment
|
||||||
|
* Verify installation with `uv pip list | grep pythonbpf` or `pip list | grep pythonbpf`
|
||||||
|
* Try reinstalling: `uv pip install --force-reinstall pythonbpf` or `pip install --force-reinstall pythonbpf`
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Now that you have PythonBPF installed, continue to the {doc}`quickstart` guide to write your first BPF program!
|
||||||
249
docs/getting-started/quickstart.md
Normal file
249
docs/getting-started/quickstart.md
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
# Quick Start
|
||||||
|
|
||||||
|
This guide will walk you through creating your first BPF program with PythonBPF.
|
||||||
|
|
||||||
|
## Your First BPF Program
|
||||||
|
|
||||||
|
Let's create a simple "Hello World" program that prints a message every time a process is executed on your system.
|
||||||
|
|
||||||
|
### Step 1: Create the Program
|
||||||
|
|
||||||
|
Create a new file called `hello_world.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, section, bpfglobal, BPF, trace_pipe
|
||||||
|
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!")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
b = BPF()
|
||||||
|
b.load()
|
||||||
|
b.attach_all()
|
||||||
|
trace_pipe()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Run the Program
|
||||||
|
|
||||||
|
Run the program with sudo (required for BPF operations):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo python3 hello_world.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: See it in Action
|
||||||
|
|
||||||
|
Open another terminal and run any command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls
|
||||||
|
echo "test"
|
||||||
|
date
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see "Hello, World!" printed in the first terminal for each command executed!
|
||||||
|
|
||||||
|
Press `Ctrl+C` to stop the program.
|
||||||
|
|
||||||
|
## Understanding the Code
|
||||||
|
|
||||||
|
Let's break down what each part does:
|
||||||
|
|
||||||
|
### Imports
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, section, bpfglobal, BPF, trace_pipe
|
||||||
|
from ctypes import c_void_p, c_int64
|
||||||
|
```
|
||||||
|
|
||||||
|
* `bpf` - Decorator to mark functions for BPF compilation
|
||||||
|
* `section` - Decorator to specify which kernel event to attach to
|
||||||
|
* `bpfglobal` - Decorator for BPF global variables
|
||||||
|
* `BPF` - Class to compile, load, and attach BPF programs
|
||||||
|
* `trace_pipe` - Utility to read kernel trace output (similar to BCC)
|
||||||
|
* `c_void_p`, `c_int64` - C types for function signatures
|
||||||
|
|
||||||
|
### The BPF Function
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_execve")
|
||||||
|
def hello_world(ctx: c_void_p) -> c_int64:
|
||||||
|
print("Hello, World!")
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
* `@bpf` - Marks this function to be compiled to BPF bytecode
|
||||||
|
* `@section("tracepoint/syscalls/sys_enter_execve")` - Attaches to the execve syscall tracepoint (called when processes start)
|
||||||
|
* `ctx: c_void_p` - Context parameter (required for all BPF functions)
|
||||||
|
* `print()` - the PythonBPF API for `bpf_printk` helper function
|
||||||
|
* `return 0` - BPF functions must return an integer
|
||||||
|
|
||||||
|
### License Declaration
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
```
|
||||||
|
|
||||||
|
* The Linux kernel requires BPF programs to declare a license
|
||||||
|
* Most kernel features require GPL-compatible licenses
|
||||||
|
* This is defined as a BPF global variable
|
||||||
|
|
||||||
|
### Compilation and Execution
|
||||||
|
|
||||||
|
```python
|
||||||
|
b = BPF()
|
||||||
|
b.load()
|
||||||
|
b.attach_all()
|
||||||
|
trace_pipe()
|
||||||
|
```
|
||||||
|
|
||||||
|
* `BPF()` - Creates a BPF object and compiles the current file
|
||||||
|
* `b.load()` - Loads the compiled BPF program into the kernel
|
||||||
|
* `b.attach_all()` - Attaches all BPF programs to their specified hooks
|
||||||
|
* `trace_pipe()` - Reads and displays output from the kernel trace buffer
|
||||||
|
|
||||||
|
Alternatively, you can also use the `compile()` function to compile the BPF code to an object file:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import compile
|
||||||
|
```
|
||||||
|
|
||||||
|
This object file can then be loaded using any other userspace library in any language.
|
||||||
|
|
||||||
|
## Next Example: Tracking Process IDs
|
||||||
|
|
||||||
|
Let's make a more interesting program that tracks which processes are being created:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, section, bpfglobal, BPF, trace_pipe
|
||||||
|
from pythonbpf.helper import pid
|
||||||
|
from ctypes import c_void_p, c_int64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_execve")
|
||||||
|
def track_exec(ctx: c_void_p) -> c_int64:
|
||||||
|
process_id = pid()
|
||||||
|
print(f"Process with PID: {process_id} is starting")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
b = BPF()
|
||||||
|
b.load()
|
||||||
|
b.attach_all()
|
||||||
|
trace_pipe()
|
||||||
|
```
|
||||||
|
|
||||||
|
This program uses BPF helper functions:
|
||||||
|
|
||||||
|
* `pid()` - Gets the current process ID
|
||||||
|
|
||||||
|
Run it with `sudo python3 track_exec.py` and watch processes being created!
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Tracepoints
|
||||||
|
|
||||||
|
Tracepoints are predefined hooks in the kernel. Common ones include:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# System calls
|
||||||
|
@section("tracepoint/syscalls/sys_enter_execve")
|
||||||
|
@section("tracepoint/syscalls/sys_enter_clone")
|
||||||
|
@section("tracepoint/syscalls/sys_enter_open")
|
||||||
|
|
||||||
|
# Scheduler events
|
||||||
|
@section("tracepoint/sched/sched_process_fork")
|
||||||
|
@section("tracepoint/sched/sched_switch")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kprobes
|
||||||
|
|
||||||
|
Kprobes allow you to attach to any kernel function:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@section("kprobe/do_sys_open")
|
||||||
|
def trace_open(ctx: c_void_p) -> c_int64:
|
||||||
|
print("File is being opened")
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### XDP (eXpress Data Path)
|
||||||
|
|
||||||
|
For network packet processing:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf.helper import XDP_PASS
|
||||||
|
|
||||||
|
@section("xdp")
|
||||||
|
def xdp_pass(ctx: c_void_p) -> c_int64:
|
||||||
|
return XDP_PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Always include a LICENSE** - Required by the kernel
|
||||||
|
2. **Use type hints** - Required by PythonBPF to generate correct code
|
||||||
|
3. **Return the correct type** - Match the expected return type for your program type
|
||||||
|
4. **Test incrementally** - Start simple and add complexity gradually
|
||||||
|
5. **Check kernel logs** - Use `dmesg` to see BPF verifier messages if loading fails
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
### Program Won't Load
|
||||||
|
|
||||||
|
If your BPF program fails to load:
|
||||||
|
|
||||||
|
* Check `dmesg` for verifier error messages
|
||||||
|
* Ensure your LICENSE is GPL-compatible
|
||||||
|
* Verify you're using supported BPF features
|
||||||
|
* Make sure return types match function signatures
|
||||||
|
|
||||||
|
### No Output
|
||||||
|
|
||||||
|
If you don't see output:
|
||||||
|
|
||||||
|
* Verify the tracepoint/kprobe is being triggered
|
||||||
|
* Check that you're running with sudo
|
||||||
|
* Ensure `/sys/kernel/tracing/trace_pipe` is accessible
|
||||||
|
|
||||||
|
### Compilation Errors
|
||||||
|
|
||||||
|
If compilation fails:
|
||||||
|
|
||||||
|
* Check that `llc` is installed and in your PATH
|
||||||
|
* Verify your Python syntax is correct
|
||||||
|
* Ensure all imported types are from `ctypes`
|
||||||
|
* In the worst case, compile object files manually using `compile_to_ir()` and `llc` to get detailed errors
|
||||||
|
|
||||||
|
### Verification Failure
|
||||||
|
|
||||||
|
If verification fails:
|
||||||
|
|
||||||
|
* Compile the object files using `compile()` function instead of loading directly
|
||||||
|
* Run `sudo check.sh check <bpf>.o` to get detailed verification output
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Now that you understand the basics, explore:
|
||||||
|
|
||||||
|
* {doc}`../user-guide/decorators` - Learn about all available decorators
|
||||||
|
* {doc}`../user-guide/maps` - Use BPF maps for data storage and communication
|
||||||
|
* {doc}`../user-guide/structs` - Define custom data structures
|
||||||
|
* {doc}`../user-guide/helpers` - Discover all available BPF helper functions
|
||||||
|
* [Examples directory](https://github.com/pythonbpf/Python-BPF/tree/master/examples) - See more complex examples
|
||||||
95
docs/index.md
Normal file
95
docs/index.md
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
# PythonBPF Documentation
|
||||||
|
|
||||||
|
Welcome to **PythonBPF** - a Python frontend for writing eBPF programs without embedding C code. PythonBPF uses [llvmlite](https://github.com/numba/llvmlite) to generate LLVM IR and compiles directly to eBPF object files that can be loaded into the Linux kernel.
|
||||||
|
|
||||||
|
```{note}
|
||||||
|
This project is under active development.
|
||||||
|
```
|
||||||
|
|
||||||
|
## What is PythonBPF?
|
||||||
|
|
||||||
|
PythonBPF is an LLVM IR generator for eBPF programs written in Python. It provides:
|
||||||
|
|
||||||
|
* **Pure Python syntax** - Write eBPF programs in Python using familiar decorators and type annotations
|
||||||
|
* **Direct compilation** - Compile to LLVM object files without relying on BCC
|
||||||
|
* **Full eBPF features** - Support for maps, helpers, global definitions, and more
|
||||||
|
* **Integration with libbpf** - Works with [pylibbpf](https://github.com/pythonbpf/pylibbpf) for object loading and execution
|
||||||
|
|
||||||
|
## Quick Example
|
||||||
|
|
||||||
|
Here's a simple "Hello World" BPF program that traces process creation:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, section, bpfglobal, BPF, trace_pipe
|
||||||
|
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!")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
b = BPF()
|
||||||
|
b.load()
|
||||||
|
b.attach_all()
|
||||||
|
trace_pipe()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* Generate eBPF programs directly using Python syntax
|
||||||
|
* 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 bindings for libbpf
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
```{toctree}
|
||||||
|
:maxdepth: 2
|
||||||
|
:caption: Getting Started
|
||||||
|
|
||||||
|
getting-started/index
|
||||||
|
getting-started/installation
|
||||||
|
getting-started/quickstart
|
||||||
|
```
|
||||||
|
|
||||||
|
```{toctree}
|
||||||
|
:maxdepth: 2
|
||||||
|
:caption: User Guide
|
||||||
|
|
||||||
|
user-guide/index
|
||||||
|
user-guide/decorators
|
||||||
|
user-guide/maps
|
||||||
|
user-guide/structs
|
||||||
|
user-guide/compilation
|
||||||
|
user-guide/helpers
|
||||||
|
```
|
||||||
|
|
||||||
|
```{toctree}
|
||||||
|
:maxdepth: 2
|
||||||
|
:caption: API Reference
|
||||||
|
|
||||||
|
api/index
|
||||||
|
```
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
* **GitHub Repository**: [pythonbpf/Python-BPF](https://github.com/pythonbpf/Python-BPF)
|
||||||
|
* **PyPI Package**: [pythonbpf](https://pypi.org/project/pythonbpf/)
|
||||||
|
* **Video Demo**: [YouTube](https://www.youtube.com/watch?v=eFVhLnWFxtE)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
PythonBPF is licensed under the Apache License 2.0.
|
||||||
|
|
||||||
|
## Indices and tables
|
||||||
|
|
||||||
|
* {ref}`genindex`
|
||||||
|
* {ref}`modindex`
|
||||||
|
* {ref}`search`
|
||||||
4
docs/requirements.txt
Normal file
4
docs/requirements.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
myst-parser>=2.0
|
||||||
|
sphinx>=7.0
|
||||||
|
sphinx-copybutton
|
||||||
|
sphinx-rtd-theme>=2.0
|
||||||
432
docs/user-guide/compilation.md
Normal file
432
docs/user-guide/compilation.md
Normal file
@ -0,0 +1,432 @@
|
|||||||
|
# Compilation
|
||||||
|
|
||||||
|
PythonBPF provides several functions and classes for compiling Python code into BPF bytecode and loading it into the kernel.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The compilation process transforms Python code into executable BPF programs:
|
||||||
|
|
||||||
|
1. **Python AST** → LLVM IR generation (using llvmlite)
|
||||||
|
2. **LLVM IR** → BPF bytecode (using llc)
|
||||||
|
3. **BPF Object** → Kernel loading (using libbpf)
|
||||||
|
|
||||||
|
## Compilation Functions
|
||||||
|
|
||||||
|
### compile_to_ir()
|
||||||
|
|
||||||
|
Compile Python source to LLVM Intermediate Representation.
|
||||||
|
|
||||||
|
#### Signature
|
||||||
|
|
||||||
|
```python
|
||||||
|
def compile_to_ir(filename: str, output: str, loglevel=logging.WARNING)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
* `filename` - Path to the Python source file to compile
|
||||||
|
* `output` - Path where the LLVM IR file (.ll) should be written
|
||||||
|
* `loglevel` - Logging level (default: `logging.WARNING`)
|
||||||
|
|
||||||
|
#### Usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import compile_to_ir
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Compile to LLVM IR
|
||||||
|
compile_to_ir(
|
||||||
|
filename="my_bpf_program.py",
|
||||||
|
output="my_bpf_program.ll",
|
||||||
|
loglevel=logging.DEBUG
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
This function generates an `.ll` file containing LLVM IR, which is human-readable assembly-like code. This is useful for:
|
||||||
|
|
||||||
|
* Debugging compilation issues
|
||||||
|
* Understanding code generation
|
||||||
|
|
||||||
|
### compile()
|
||||||
|
|
||||||
|
Compile Python source to BPF object file.
|
||||||
|
|
||||||
|
#### Signature
|
||||||
|
|
||||||
|
```python
|
||||||
|
def compile(filename: str = None, output: str = None, loglevel=logging.WARNING)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
* `filename` - Path to the Python source file (default: calling file)
|
||||||
|
* `output` - Path for the output object file (default: same name with `.o` extension)
|
||||||
|
* `loglevel` - Logging level (default: `logging.WARNING`)
|
||||||
|
|
||||||
|
#### Usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import compile
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Compile current file
|
||||||
|
compile()
|
||||||
|
|
||||||
|
# Compile specific file
|
||||||
|
compile(filename="my_program.py", output="my_program.o")
|
||||||
|
|
||||||
|
# Compile with debug logging
|
||||||
|
compile(loglevel=logging.DEBUG)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
This function generates a `.o` file containing BPF bytecode that can be:
|
||||||
|
|
||||||
|
* Loaded into the kernel
|
||||||
|
* Inspected with `bpftool`
|
||||||
|
* Verified with the BPF verifier
|
||||||
|
* Distributed as a compiled binary
|
||||||
|
|
||||||
|
### BPF Class
|
||||||
|
|
||||||
|
The `BPF` class provides a high-level interface to compile, load, and attach BPF programs.
|
||||||
|
|
||||||
|
#### Signature
|
||||||
|
|
||||||
|
```python
|
||||||
|
class BPF:
|
||||||
|
def __init__(self, filename: str = None, loglevel=logging.WARNING)
|
||||||
|
def load(self)
|
||||||
|
def attach_all(self)
|
||||||
|
def load_and_attach(self)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
* `filename` - Path to Python source file (default: calling file)
|
||||||
|
* `loglevel` - Logging level (default: `logging.WARNING`)
|
||||||
|
|
||||||
|
#### Methods
|
||||||
|
|
||||||
|
##### __init__()
|
||||||
|
|
||||||
|
Create a BPF object and compile the source.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import BPF
|
||||||
|
|
||||||
|
# Compile current file
|
||||||
|
b = BPF()
|
||||||
|
|
||||||
|
# Compile specific file
|
||||||
|
b = BPF(filename="my_program.py")
|
||||||
|
```
|
||||||
|
|
||||||
|
##### load()
|
||||||
|
|
||||||
|
Load the compiled BPF program into the kernel.
|
||||||
|
|
||||||
|
```python
|
||||||
|
b = BPF()
|
||||||
|
b.load()
|
||||||
|
```
|
||||||
|
|
||||||
|
This method:
|
||||||
|
* Loads the BPF object file into the kernel
|
||||||
|
* Creates maps
|
||||||
|
* Verifies the BPF program
|
||||||
|
* Returns a `BpfObject` instance
|
||||||
|
|
||||||
|
##### attach_all()
|
||||||
|
|
||||||
|
Attach all BPF programs to their specified hooks.
|
||||||
|
|
||||||
|
```python
|
||||||
|
b = BPF()
|
||||||
|
b.load()
|
||||||
|
b.attach_all()
|
||||||
|
```
|
||||||
|
|
||||||
|
This method:
|
||||||
|
* Attaches tracepoints
|
||||||
|
* Attaches kprobes/kretprobes
|
||||||
|
* Attaches XDP programs
|
||||||
|
* Enables all hooks
|
||||||
|
|
||||||
|
##### load_and_attach()
|
||||||
|
|
||||||
|
Convenience method that loads and attaches in one call.
|
||||||
|
|
||||||
|
```python
|
||||||
|
b = BPF()
|
||||||
|
b.load_and_attach()
|
||||||
|
```
|
||||||
|
|
||||||
|
Equivalent to:
|
||||||
|
```python
|
||||||
|
b = BPF()
|
||||||
|
b.load()
|
||||||
|
b.attach_all()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Example
|
||||||
|
|
||||||
|
Here's a complete example showing the compilation workflow:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, section, bpfglobal, BPF, trace_pipe
|
||||||
|
from ctypes import c_void_p, c_int64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_execve")
|
||||||
|
def trace_exec(ctx: c_void_p) -> c_int64:
|
||||||
|
print("Process started")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Method 1: Simple compilation and loading
|
||||||
|
b = BPF()
|
||||||
|
b.load_and_attach()
|
||||||
|
trace_pipe()
|
||||||
|
|
||||||
|
# Method 2: Step-by-step
|
||||||
|
# b = BPF()
|
||||||
|
# b.load()
|
||||||
|
# b.attach_all()
|
||||||
|
# trace_pipe()
|
||||||
|
|
||||||
|
# Method 3: Manual compilation
|
||||||
|
# from pythonbpf import compile
|
||||||
|
# compile(filename="my_program.py", output="my_program.o")
|
||||||
|
# # Then load with pylibbpf directly
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compilation Pipeline Details
|
||||||
|
|
||||||
|
### AST Parsing
|
||||||
|
|
||||||
|
The Python `ast` module parses your source code:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import ast
|
||||||
|
tree = ast.parse(source_code, filename)
|
||||||
|
```
|
||||||
|
|
||||||
|
The AST is then walked to find:
|
||||||
|
* Functions decorated with `@bpf`
|
||||||
|
* Classes decorated with `@struct`
|
||||||
|
* Map definitions with `@map`
|
||||||
|
* Global variables with `@bpfglobal`
|
||||||
|
|
||||||
|
### IR Generation
|
||||||
|
|
||||||
|
PythonBPF uses `llvmlite` to generate LLVM IR:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from llvmlite import ir
|
||||||
|
|
||||||
|
# Create module
|
||||||
|
module = ir.Module(name='bpf_module')
|
||||||
|
module.triple = 'bpf'
|
||||||
|
|
||||||
|
# Generate IR for each BPF function
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Key aspects of IR generation:
|
||||||
|
|
||||||
|
* Type conversion (Python types → LLVM types)
|
||||||
|
* Function definitions
|
||||||
|
* Map declarations
|
||||||
|
* Global variable initialization
|
||||||
|
* Debug information
|
||||||
|
|
||||||
|
### BPF Compilation
|
||||||
|
|
||||||
|
The LLVM IR is compiled to BPF bytecode using `llc`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
llc -march=bpf -filetype=obj input.ll -o output.o
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kernel Loading
|
||||||
|
|
||||||
|
The compiled object is loaded using `pylibbpf`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pylibbpf import BpfObject
|
||||||
|
|
||||||
|
obj = BpfObject(path="program.o")
|
||||||
|
obj.load()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debugging Compilation
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
Enable debug logging to see compilation details:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import logging
|
||||||
|
from pythonbpf import BPF
|
||||||
|
|
||||||
|
b = BPF(loglevel=logging.DEBUG)
|
||||||
|
```
|
||||||
|
|
||||||
|
This will show:
|
||||||
|
* AST parsing details
|
||||||
|
* IR generation steps
|
||||||
|
* Compilation commands
|
||||||
|
* Loading status
|
||||||
|
|
||||||
|
### Inspecting LLVM IR
|
||||||
|
|
||||||
|
Generate and inspect the IR file:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import compile_to_ir
|
||||||
|
|
||||||
|
compile_to_ir("program.py", "program.ll")
|
||||||
|
```
|
||||||
|
|
||||||
|
Then examine `program.ll` to understand the generated code.
|
||||||
|
|
||||||
|
### Using bpftool
|
||||||
|
|
||||||
|
Inspect compiled objects with `bpftool`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Show program info
|
||||||
|
bpftool prog show
|
||||||
|
|
||||||
|
# Dump program instructions
|
||||||
|
bpftool prog dump xlated id <ID>
|
||||||
|
|
||||||
|
# Dump program JIT code
|
||||||
|
bpftool prog dump jited id <ID>
|
||||||
|
|
||||||
|
# Show maps
|
||||||
|
bpftool map show
|
||||||
|
|
||||||
|
# Dump map contents
|
||||||
|
bpftool map dump id <ID>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verifier Errors
|
||||||
|
|
||||||
|
If the kernel verifier rejects your program:
|
||||||
|
|
||||||
|
* Check `dmesg` for detailed error messages:
|
||||||
|
```bash
|
||||||
|
sudo dmesg | tail -50
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compilation Options
|
||||||
|
|
||||||
|
### Optimization Levels
|
||||||
|
|
||||||
|
While PythonBPF doesn't expose optimization flags directly, you can:
|
||||||
|
|
||||||
|
1. Manually compile IR with specific flags:
|
||||||
|
```bash
|
||||||
|
llc -march=bpf -O2 -filetype=obj program.ll -o program.o
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Modify the compilation pipeline in your code
|
||||||
|
|
||||||
|
### Debug Information
|
||||||
|
|
||||||
|
PythonBPF automatically generates debug information (DWARF) for:
|
||||||
|
|
||||||
|
* Function names
|
||||||
|
* Variable names
|
||||||
|
* Type information
|
||||||
|
|
||||||
|
This helps with:
|
||||||
|
* Stack traces
|
||||||
|
* Debugging with `bpftool`
|
||||||
|
* Source-level debugging
|
||||||
|
|
||||||
|
## Working with Compiled Objects
|
||||||
|
|
||||||
|
### Loading Pre-compiled Objects
|
||||||
|
|
||||||
|
You can load previously compiled objects:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pylibbpf import BpfObject
|
||||||
|
|
||||||
|
# Load object file
|
||||||
|
obj = BpfObject(path="my_program.o")
|
||||||
|
obj.load()
|
||||||
|
|
||||||
|
# Attach programs
|
||||||
|
# (specific attachment depends on program type)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Distribution
|
||||||
|
|
||||||
|
Distribute compiled BPF objects:
|
||||||
|
|
||||||
|
1. Compile once:
|
||||||
|
```python
|
||||||
|
from pythonbpf import compile
|
||||||
|
compile(filename="program.py", output="program.o")
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Ship `program.o` file
|
||||||
|
|
||||||
|
3. Load on target systems:
|
||||||
|
```python
|
||||||
|
from pylibbpf import BpfObject
|
||||||
|
obj = BpfObject(path="program.o")
|
||||||
|
obj.load()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Version Compatibility
|
||||||
|
|
||||||
|
BPF objects are generally compatible across kernel versions, but:
|
||||||
|
|
||||||
|
* Some features require specific kernel versions
|
||||||
|
* Helper functions may not be available on older kernels
|
||||||
|
* BTF (BPF Type Format) requirements vary
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Compilation Fails
|
||||||
|
|
||||||
|
If compilation fails:
|
||||||
|
* Check Python syntax
|
||||||
|
* Verify all decorators are correct
|
||||||
|
* Ensure type hints are present
|
||||||
|
* Check for unsupported Python features
|
||||||
|
|
||||||
|
### Loading Fails
|
||||||
|
|
||||||
|
If loading fails:
|
||||||
|
* Check `dmesg` for verifier errors
|
||||||
|
* Verify LICENSE is set correctly
|
||||||
|
* Ensure helper functions are valid
|
||||||
|
* Check map definitions
|
||||||
|
|
||||||
|
### Programs Don't Attach
|
||||||
|
|
||||||
|
If attachment fails:
|
||||||
|
* Verify section names are correct
|
||||||
|
* Check that hooks exist on your kernel
|
||||||
|
* Ensure you have sufficient permissions
|
||||||
|
* Verify kernel version supports the feature
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
* Learn about {doc}`helpers` for available BPF helper functions
|
||||||
|
* Explore {doc}`maps` for data storage
|
||||||
|
* See {doc}`decorators` for compilation markers
|
||||||
448
docs/user-guide/decorators.md
Normal file
448
docs/user-guide/decorators.md
Normal file
@ -0,0 +1,448 @@
|
|||||||
|
# Decorators
|
||||||
|
|
||||||
|
Decorators are the primary way to mark Python code for BPF compilation. PythonBPF provides five core decorators that control how your code is transformed into eBPF bytecode.
|
||||||
|
|
||||||
|
## @bpf
|
||||||
|
|
||||||
|
The `@bpf` decorator marks functions or classes for BPF compilation.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
def my_function(ctx):
|
||||||
|
# This function will be compiled to BPF bytecode
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
Any function or class decorated with `@bpf` will be processed by the PythonBPF compiler and transformed into LLVM IR, then compiled to BPF bytecode. This is the fundamental decorator that enables BPF compilation.
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
* Must be used on top-level functions or classes
|
||||||
|
* The function must have proper type hints
|
||||||
|
* Return types must be BPF-compatible
|
||||||
|
* Only BPF-compatible operations are allowed inside
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, section
|
||||||
|
from ctypes import c_void_p, c_int64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_execve")
|
||||||
|
def trace_exec(ctx: c_void_p) -> c_int64:
|
||||||
|
print("Process started")
|
||||||
|
return c_int64(0)
|
||||||
|
```
|
||||||
|
|
||||||
|
## @section
|
||||||
|
|
||||||
|
The `@section(name)` decorator specifies which kernel hook to attach the BPF program to.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, section
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_open")
|
||||||
|
def trace_open(ctx):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Section Types
|
||||||
|
|
||||||
|
#### Tracepoints
|
||||||
|
|
||||||
|
Tracepoints are stable kernel hooks defined in `/sys/kernel/tracing/events/`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# System call tracepoints
|
||||||
|
@section("tracepoint/syscalls/sys_enter_execve")
|
||||||
|
@section("tracepoint/syscalls/sys_enter_clone")
|
||||||
|
@section("tracepoint/syscalls/sys_enter_open")
|
||||||
|
@section("tracepoint/syscalls/sys_exit_read")
|
||||||
|
|
||||||
|
# Scheduler tracepoints
|
||||||
|
@section("tracepoint/sched/sched_process_fork")
|
||||||
|
@section("tracepoint/sched/sched_process_exit")
|
||||||
|
@section("tracepoint/sched/sched_switch")
|
||||||
|
|
||||||
|
# Block I/O tracepoints
|
||||||
|
@section("tracepoint/block/block_rq_insert")
|
||||||
|
@section("tracepoint/block/block_rq_complete")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Kprobes
|
||||||
|
|
||||||
|
Kprobes allow attaching to any kernel function:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@section("kprobe/do_sys_open")
|
||||||
|
def trace_sys_open(ctx):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@section("kprobe/__x64_sys_write")
|
||||||
|
def trace_write(ctx):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Kretprobes
|
||||||
|
|
||||||
|
Kretprobes trigger when a kernel function returns:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@section("kretprobe/do_sys_open")
|
||||||
|
def trace_open_return(ctx):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
#### XDP (eXpress Data Path)
|
||||||
|
|
||||||
|
For network packet processing at the earliest point:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf.helper import XDP_PASS
|
||||||
|
from ctypes import c_void_p, c_int64
|
||||||
|
|
||||||
|
@section("xdp")
|
||||||
|
def xdp_prog(ctx: c_void_p) -> c_int64:
|
||||||
|
# XDP_PASS, XDP_DROP, XDP_ABORTED constants available from pythonbpf.helper
|
||||||
|
return XDP_PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
### Finding Tracepoints
|
||||||
|
|
||||||
|
To find available tracepoints on your system:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all tracepoints
|
||||||
|
ls /sys/kernel/tracing/events/
|
||||||
|
|
||||||
|
# List syscall tracepoints
|
||||||
|
ls /sys/kernel/tracing/events/syscalls/
|
||||||
|
|
||||||
|
# View tracepoint format
|
||||||
|
cat /sys/kernel/tracing/events/syscalls/sys_enter_open/format
|
||||||
|
```
|
||||||
|
|
||||||
|
## @map
|
||||||
|
|
||||||
|
The `@map` decorator marks a function as a BPF map definition.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, map
|
||||||
|
from pythonbpf.maps import HashMap
|
||||||
|
from ctypes import c_uint32, c_uint64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def my_map() -> HashMap:
|
||||||
|
return HashMap(key=c_uint32, value=c_uint64, max_entries=1024)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
Maps are BPF data structures used to:
|
||||||
|
|
||||||
|
* Store state between BPF program invocations
|
||||||
|
* Communicate data between BPF programs
|
||||||
|
* Share data with userspace
|
||||||
|
|
||||||
|
The function must return a map type (HashMap, PerfEventArray, RingBuffer) and the return type must be annotated.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, map, section
|
||||||
|
from pythonbpf.maps import HashMap
|
||||||
|
from pythonbpf.helper import pid
|
||||||
|
from ctypes import c_void_p, c_int64, c_uint32, c_uint64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def process_count() -> HashMap:
|
||||||
|
return HashMap(key=c_uint32, value=c_uint64, max_entries=4096)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_clone")
|
||||||
|
def count_clones(ctx: c_void_p) -> c_int64:
|
||||||
|
process_id = pid()
|
||||||
|
count = process_count.lookup(process_id)
|
||||||
|
if count:
|
||||||
|
process_count.update(process_id, count + 1)
|
||||||
|
else:
|
||||||
|
process_count.update(process_id, c_uint64(1))
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
See {doc}`maps` for more details on available map types.
|
||||||
|
|
||||||
|
## @struct
|
||||||
|
|
||||||
|
The `@struct` decorator marks a class as a BPF struct definition.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, struct
|
||||||
|
from ctypes import c_uint64, c_uint32
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class Event:
|
||||||
|
timestamp: c_uint64
|
||||||
|
pid: c_uint32
|
||||||
|
cpu: c_uint32
|
||||||
|
```
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
Structs allow you to define custom data types for use in BPF programs. They can be used:
|
||||||
|
|
||||||
|
* As map keys and values
|
||||||
|
* For perf event output
|
||||||
|
* In ring buffer submissions
|
||||||
|
* As local variables
|
||||||
|
|
||||||
|
### Field Types
|
||||||
|
|
||||||
|
Supported field types include:
|
||||||
|
|
||||||
|
* **Integer types**: `c_int8`, `c_int16`, `c_int32`, `c_int64`, `c_uint8`, `c_uint16`, `c_uint32`, `c_uint64`
|
||||||
|
* **Pointers**: `c_void_p`, `c_char_p`
|
||||||
|
* **Fixed strings**: `str(N)` where N is the size (e.g., `str(16)`)
|
||||||
|
* **Nested structs**: Other `@struct` decorated classes
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, struct, map, section
|
||||||
|
from pythonbpf.maps import RingBuffer
|
||||||
|
from pythonbpf.helper import pid, ktime
|
||||||
|
from ctypes import c_void_p, c_int64, c_uint64, c_uint32
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class ProcessEvent:
|
||||||
|
timestamp: c_uint64
|
||||||
|
pid: c_uint32
|
||||||
|
comm: str(16)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def events() -> RingBuffer:
|
||||||
|
return RingBuffer(max_entries=4096)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_execve")
|
||||||
|
def track_processes(ctx: c_void_p) -> c_int64:
|
||||||
|
event = ProcessEvent()
|
||||||
|
event.timestamp = ktime()
|
||||||
|
event.pid = pid()
|
||||||
|
comm(event.comm) # Fills event.comm with process name
|
||||||
|
|
||||||
|
events.output(event)
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
See {doc}`structs` for more details on working with structs.
|
||||||
|
|
||||||
|
## @bpfglobal
|
||||||
|
|
||||||
|
The `@bpfglobal` decorator marks a function as a BPF global variable definition.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, bpfglobal
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
BPF global variables are values that:
|
||||||
|
|
||||||
|
* Are initialized when the program loads
|
||||||
|
* Can be read by all BPF functions
|
||||||
|
* Must be constant (cannot be modified at runtime in current implementation)
|
||||||
|
|
||||||
|
### Common Global Variables
|
||||||
|
|
||||||
|
#### LICENSE (Required)
|
||||||
|
|
||||||
|
Every BPF program must declare a license:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
```
|
||||||
|
|
||||||
|
Valid licenses include:
|
||||||
|
* `"GPL"` - GNU General Public License
|
||||||
|
* `"GPL v2"` - GPL version 2
|
||||||
|
* `"Dual BSD/GPL"` - Dual licensed
|
||||||
|
* `"Dual MIT/GPL"` - Dual licensed
|
||||||
|
|
||||||
|
```{warning}
|
||||||
|
Many BPF features require a GPL-compatible license. Using a non-GPL license may prevent your program from loading or accessing certain kernel features.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Custom Global Variables
|
||||||
|
|
||||||
|
You can define other global variables:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def DEBUG_MODE() -> int:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def MAX_EVENTS() -> int:
|
||||||
|
return 1000
|
||||||
|
```
|
||||||
|
|
||||||
|
These can be referenced in your BPF functions, though modifying them at runtime is currently not supported.
|
||||||
|
|
||||||
|
## Combining Decorators
|
||||||
|
|
||||||
|
Decorators are often used together. The order matters:
|
||||||
|
|
||||||
|
### Correct Order
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf # Always first
|
||||||
|
@section("...") # Section before other decorators
|
||||||
|
def my_function():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@bpf # Always first
|
||||||
|
@map # Map/struct/bpfglobal after @bpf
|
||||||
|
def my_map():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@bpf # Always first
|
||||||
|
@struct # Map/struct/bpfglobal after @bpf
|
||||||
|
class MyStruct:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@bpf # Always first
|
||||||
|
@bpfglobal # Map/struct/bpfglobal after @bpf
|
||||||
|
def LICENSE():
|
||||||
|
return "GPL"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples by Use Case
|
||||||
|
|
||||||
|
#### Simple Tracepoint
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_open")
|
||||||
|
def trace_open(ctx: c_void_p) -> c_int64:
|
||||||
|
return c_int64(0)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Map Definition
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def counters() -> HashMap:
|
||||||
|
return HashMap(key=c_uint32, value=c_uint64, max_entries=256)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Struct Definition
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class Event:
|
||||||
|
timestamp: c_uint64
|
||||||
|
value: c_uint32
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Global Variable
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Always use @bpf first** - It must be the outermost decorator
|
||||||
|
2. **Provide type hints** - Required for proper code generation
|
||||||
|
3. **Test incrementally** - Verify each component works before combining
|
||||||
|
|
||||||
|
## Common Errors
|
||||||
|
|
||||||
|
### Missing @bpf Decorator
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Wrong - missing @bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_open")
|
||||||
|
def my_func(ctx):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Correct
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_open")
|
||||||
|
def my_func(ctx):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wrong Decorator Order
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Wrong - @section before @bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_open")
|
||||||
|
@bpf
|
||||||
|
def my_func(ctx):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Correct
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_open")
|
||||||
|
def my_func(ctx):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Missing Type Hints
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Wrong - no type hints
|
||||||
|
@bpf
|
||||||
|
def my_func(ctx):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Correct
|
||||||
|
@bpf
|
||||||
|
def my_func(ctx: c_void_p) -> c_int64:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
* Learn about {doc}`maps` for data storage and communication
|
||||||
|
* Explore {doc}`structs` for defining custom data types
|
||||||
|
* Understand {doc}`compilation` to see how code is transformed
|
||||||
|
* Check out {doc}`helpers` for available BPF helper functions
|
||||||
503
docs/user-guide/helpers.md
Normal file
503
docs/user-guide/helpers.md
Normal file
@ -0,0 +1,503 @@
|
|||||||
|
# Helper Functions and Utilities
|
||||||
|
|
||||||
|
PythonBPF provides helper functions and utilities for BPF programs and userspace code.
|
||||||
|
|
||||||
|
```{note}
|
||||||
|
**Work in Progress:** PythonBPF is under active development. We are constantly adding support for more helpers, kfuncs, and map types. Check back for updates!
|
||||||
|
```
|
||||||
|
For comprehensive documentation on BPF helpers, see the [eBPF Helper Functions documentation on ebpf.io](https://ebpf.io/what-is-ebpf/#helper-calls).
|
||||||
|
|
||||||
|
## BPF Helper Functions
|
||||||
|
|
||||||
|
BPF helper functions are kernel-provided functions that BPF programs can call to interact with the system. PythonBPF exposes these through the `pythonbpf.helper` module.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf.helper import pid, ktime, comm
|
||||||
|
```
|
||||||
|
|
||||||
|
### Process and Task Information
|
||||||
|
|
||||||
|
#### pid()
|
||||||
|
|
||||||
|
Get the current process ID.
|
||||||
|
|
||||||
|
> **Linux Kernel Helper:** `bpf_get_current_pid_tgid()`
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf.helper import pid
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_open")
|
||||||
|
def trace_open(ctx: c_void_p) -> c_int64:
|
||||||
|
process_id = pid()
|
||||||
|
print(f"Process {process_id} opened a file")
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** `c_int32` - The process ID of the current task
|
||||||
|
|
||||||
|
#### comm()
|
||||||
|
|
||||||
|
Get the current process command name.
|
||||||
|
|
||||||
|
> **Linux Kernel Helper:** `bpf_get_current_comm()`
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
* `buf` - Buffer to fill with the process command name
|
||||||
|
|
||||||
|
**Returns:** `c_int64` - 0 on success, negative on error
|
||||||
|
|
||||||
|
#### uid()
|
||||||
|
|
||||||
|
Get the current user ID.
|
||||||
|
|
||||||
|
> **Linux Kernel Helper:** `bpf_get_current_uid_gid()`
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf.helper import uid
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_open")
|
||||||
|
def trace_open(ctx: c_void_p) -> c_int64:
|
||||||
|
user_id = uid()
|
||||||
|
if user_id == 0:
|
||||||
|
print("Root user opened a file")
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** `c_int32` - The user ID of the current task
|
||||||
|
|
||||||
|
### Time and Timing
|
||||||
|
|
||||||
|
#### ktime()
|
||||||
|
|
||||||
|
Get the current kernel time in nanoseconds since system boot.
|
||||||
|
|
||||||
|
> **Linux Kernel Helper:** `bpf_ktime_get_ns()`
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf.helper import ktime
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_read")
|
||||||
|
def measure_latency(ctx: c_void_p) -> c_int64:
|
||||||
|
start_time = ktime()
|
||||||
|
# Store for later comparison
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** `c_int64` - Current time in nanoseconds
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
* Measuring latency
|
||||||
|
* Timestamping events
|
||||||
|
* Rate limiting
|
||||||
|
* Timeout detection
|
||||||
|
|
||||||
|
### CPU Information
|
||||||
|
|
||||||
|
#### smp_processor_id()
|
||||||
|
|
||||||
|
Get the ID of the CPU on which the BPF program is running.
|
||||||
|
|
||||||
|
> **Linux Kernel Helper:** `bpf_get_smp_processor_id()`
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf.helper import smp_processor_id
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/sched/sched_switch")
|
||||||
|
def track_cpu(ctx: c_void_p) -> c_int64:
|
||||||
|
cpu = smp_processor_id()
|
||||||
|
print(f"Running on CPU {cpu}")
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** `c_int32` - The current CPU ID
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
* Per-CPU statistics
|
||||||
|
* Load balancing analysis
|
||||||
|
* CPU affinity tracking
|
||||||
|
|
||||||
|
### Memory Operations
|
||||||
|
|
||||||
|
#### probe_read()
|
||||||
|
|
||||||
|
Safely read data from kernel memory.
|
||||||
|
|
||||||
|
> **Linux Kernel Helper:** `bpf_probe_read()`
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf.helper import probe_read
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
def read_kernel_data(ctx: c_void_p) -> c_int64:
|
||||||
|
dst = 0
|
||||||
|
size = 8
|
||||||
|
src = ctx # kernel address
|
||||||
|
|
||||||
|
result = probe_read(dst, size, src)
|
||||||
|
if result == 0:
|
||||||
|
print(f"Read value: {dst}")
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
* `dst` - Destination buffer
|
||||||
|
* `size` - Number of bytes to read
|
||||||
|
* `src` - Source kernel address
|
||||||
|
|
||||||
|
**Returns:** `c_int64` - 0 on success, negative on error
|
||||||
|
|
||||||
|
**Safety:** This function performs bounds checking and prevents invalid memory access.
|
||||||
|
|
||||||
|
#### probe_read_str()
|
||||||
|
|
||||||
|
Safely read a null-terminated string from kernel memory.
|
||||||
|
|
||||||
|
> **Linux Kernel Helper:** `bpf_probe_read_str()`
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
* `dst` - Destination buffer (string)
|
||||||
|
* `src` - Source kernel address
|
||||||
|
|
||||||
|
**Returns:** `c_int64` - Length of string on success, negative on error
|
||||||
|
|
||||||
|
### Random Numbers
|
||||||
|
|
||||||
|
#### random()
|
||||||
|
|
||||||
|
Generate a pseudo-random 32-bit number.
|
||||||
|
|
||||||
|
> **Linux Kernel Helper:** `bpf_get_prandom_u32()`
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf.helper import random
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_open")
|
||||||
|
def sample_events(ctx: c_void_p) -> c_int64:
|
||||||
|
# Sample 1% of events
|
||||||
|
if (random() % 100) == 0:
|
||||||
|
print("Sampled event")
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** `c_int32` - A pseudo-random number
|
||||||
|
|
||||||
|
### Network Helpers
|
||||||
|
|
||||||
|
#### skb_store_bytes()
|
||||||
|
|
||||||
|
Store bytes into a socket buffer (for network programs).
|
||||||
|
|
||||||
|
> **Linux Kernel Helper:** `bpf_skb_store_bytes()`
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf.helper import skb_store_bytes
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("classifier")
|
||||||
|
def modify_packet(ctx: c_void_p) -> c_int32:
|
||||||
|
offset = 14 # Skip Ethernet header
|
||||||
|
data = b"\x00\x01\x02\x03"
|
||||||
|
size = len(data)
|
||||||
|
|
||||||
|
result = skb_store_bytes(offset, data, size)
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
* `offset` - Offset in the socket buffer
|
||||||
|
* `from_buf` - Data to write
|
||||||
|
* `size` - Number of bytes to write
|
||||||
|
* `flags` - Optional flags
|
||||||
|
|
||||||
|
**Returns:** `c_int64` - 0 on success, negative on error
|
||||||
|
|
||||||
|
## Userspace Utilities
|
||||||
|
|
||||||
|
PythonBPF provides utilities for working with BPF programs from Python userspace code.
|
||||||
|
|
||||||
|
### trace_pipe()
|
||||||
|
|
||||||
|
Read and display output from the kernel trace pipe.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import trace_pipe
|
||||||
|
|
||||||
|
# After loading and attaching BPF programs
|
||||||
|
trace_pipe()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Description:**
|
||||||
|
|
||||||
|
The `trace_pipe()` function reads from `/sys/kernel/tracing/trace_pipe` and displays BPF program output to stdout. This is the output from `print()` statements in BPF programs.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, section, bpfglobal, BPF, trace_pipe
|
||||||
|
from ctypes import c_void_p, c_int64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_execve")
|
||||||
|
def trace_exec(ctx: c_void_p) -> c_int64:
|
||||||
|
print("Process started") # This goes to trace_pipe
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
b = BPF()
|
||||||
|
b.load_and_attach()
|
||||||
|
trace_pipe() # Display BPF output
|
||||||
|
```
|
||||||
|
|
||||||
|
**Behavior:**
|
||||||
|
|
||||||
|
* Blocks until Ctrl+C is pressed
|
||||||
|
* Displays output in real-time
|
||||||
|
* Shows task name, PID, CPU, timestamp, and message
|
||||||
|
* Automatically handles trace pipe access errors
|
||||||
|
|
||||||
|
**Requirements:**
|
||||||
|
|
||||||
|
* Root or sudo access
|
||||||
|
* Accessible `/sys/kernel/tracing/trace_pipe`
|
||||||
|
|
||||||
|
### trace_fields()
|
||||||
|
|
||||||
|
Parse one line from the trace pipe into structured fields.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import trace_fields
|
||||||
|
|
||||||
|
# Read and parse trace output
|
||||||
|
task, pid, cpu, flags, ts, msg = trace_fields()
|
||||||
|
print(f"Task: {task}, PID: {pid}, CPU: {cpu}, Time: {ts}, Message: {msg}")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** Tuple of `(task, pid, cpu, flags, timestamp, message)`
|
||||||
|
|
||||||
|
* `task` - String: Task/process name (up to 16 chars)
|
||||||
|
* `pid` - Integer: Process ID
|
||||||
|
* `cpu` - Integer: CPU number
|
||||||
|
* `flags` - Bytes: Trace flags
|
||||||
|
* `timestamp` - Float: Timestamp in seconds
|
||||||
|
* `message` - String: The actual trace message
|
||||||
|
|
||||||
|
**Description:**
|
||||||
|
|
||||||
|
The `trace_fields()` function reads one line from the trace pipe and parses it into individual fields. This is useful when you need programmatic access to trace data rather than just displaying it.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, section, bpfglobal, BPF, trace_fields
|
||||||
|
from ctypes import c_void_p, c_int64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_execve")
|
||||||
|
def trace_exec(ctx: c_void_p) -> c_int64:
|
||||||
|
print(f"PID:{pid()}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
b = BPF()
|
||||||
|
b.load_and_attach()
|
||||||
|
|
||||||
|
# Process trace events
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
task, pid, cpu, flags, ts, msg = trace_fields()
|
||||||
|
print(f"[{ts:.6f}] {task}({pid}) on CPU{cpu}: {msg}")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Stopped")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Handling:**
|
||||||
|
|
||||||
|
* Raises `ValueError` if line cannot be parsed
|
||||||
|
* Skips lines about lost events
|
||||||
|
* Blocks waiting for next line
|
||||||
|
|
||||||
|
## Helper Function Examples
|
||||||
|
|
||||||
|
### Example 1: Latency Measurement
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, map, section, bpfglobal, BPF, trace_pipe
|
||||||
|
from pythonbpf.maps import HashMap
|
||||||
|
from pythonbpf.helper import pid, ktime
|
||||||
|
from ctypes import c_void_p, c_int64, c_uint32, c_uint64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def start_times() -> HashMap:
|
||||||
|
return HashMap(key=c_uint32, value=c_uint64, max_entries=4096)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_read")
|
||||||
|
def read_start(ctx: c_void_p) -> c_int64:
|
||||||
|
process_id = pid()
|
||||||
|
start = ktime()
|
||||||
|
start_times.update(process_id, start)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_exit_read")
|
||||||
|
def read_end(ctx: c_void_p) -> c_int64:
|
||||||
|
process_id = pid()
|
||||||
|
start = start_times.lookup(process_id)
|
||||||
|
|
||||||
|
if start:
|
||||||
|
latency = ktime() - start
|
||||||
|
print(f"Read latency: {latency} ns")
|
||||||
|
start_times.delete(process_id)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
b = BPF()
|
||||||
|
b.load_and_attach()
|
||||||
|
trace_pipe()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Process Tracking
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, section, bpfglobal, BPF, trace_pipe
|
||||||
|
from pythonbpf.helper import pid, uid
|
||||||
|
from ctypes import c_void_p, c_int64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_execve")
|
||||||
|
def track_exec(ctx: c_void_p) -> c_int64:
|
||||||
|
process_id = pid()
|
||||||
|
user_id = uid()
|
||||||
|
|
||||||
|
print(f"User {user_id} started process (PID: {process_id})")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
b = BPF()
|
||||||
|
b.load_and_attach()
|
||||||
|
trace_pipe()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: CPU Load Monitoring
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, map, section, bpfglobal, BPF
|
||||||
|
from pythonbpf.maps import HashMap
|
||||||
|
from pythonbpf.helper import smp_processor_id
|
||||||
|
from ctypes import c_void_p, c_int64, c_uint32, c_uint64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def cpu_counts() -> HashMap:
|
||||||
|
return HashMap(key=c_uint32, value=c_uint64, max_entries=256)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/sched/sched_switch")
|
||||||
|
def count_switches(ctx: c_void_p) -> c_int64:
|
||||||
|
cpu = smp_processor_id()
|
||||||
|
count = cpu_counts.lookup(cpu)
|
||||||
|
|
||||||
|
if count:
|
||||||
|
cpu_counts.update(cpu, count + 1)
|
||||||
|
else:
|
||||||
|
cpu_counts.update(cpu, 1)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
b = BPF()
|
||||||
|
b.load_and_attach()
|
||||||
|
|
||||||
|
import time
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# Read results
|
||||||
|
from pylibbpf import BpfMap
|
||||||
|
map_obj = BpfMap(b, cpu_counts)
|
||||||
|
for cpu, count in map_obj.items():
|
||||||
|
print(f"CPU {cpu}: {count} context switches")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 4: Event Sampling
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, section, bpfglobal, BPF, trace_pipe
|
||||||
|
from pythonbpf.helper import random, pid
|
||||||
|
from ctypes import c_void_p, c_int64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_open")
|
||||||
|
def sample_opens(ctx: c_void_p) -> c_int64:
|
||||||
|
# Sample 5% of events
|
||||||
|
if (random() % 100) < 5:
|
||||||
|
process_id = pid()
|
||||||
|
print(f"Sampled: PID {process_id} opening file")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
b = BPF()
|
||||||
|
b.load_and_attach()
|
||||||
|
trace_pipe()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Helper Not Available
|
||||||
|
|
||||||
|
If a helper function doesn't work:
|
||||||
|
* Check your kernel version (some helpers are newer)
|
||||||
|
* Ensure your LICENSE is GPL-compatible
|
||||||
|
|
||||||
|
### Trace Pipe Access Denied
|
||||||
|
|
||||||
|
If `trace_pipe()` fails:
|
||||||
|
* Run with sudo/root
|
||||||
|
* Check `/sys/kernel/tracing/` is accessible
|
||||||
|
* Verify tracing is enabled in kernel config
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Check out these examples in the `BCC-Examples/` directory that demonstrate helper functions:
|
||||||
|
|
||||||
|
* [hello_world.py](https://github.com/pythonbpf/Python-BPF/blob/main/BCC-Examples/hello_world.py) - Basic tracing with `print()`
|
||||||
|
* [sync_timing.py](https://github.com/pythonbpf/Python-BPF/blob/main/BCC-Examples/sync_timing.py) - Using `ktime()` for timing measurements
|
||||||
|
* [hello_perf_output.py](https://github.com/pythonbpf/Python-BPF/blob/main/BCC-Examples/hello_perf_output.py) - Using `pid()`, `ktime()`, and `comm()` with perf events
|
||||||
|
* [vfsreadlat.py](https://github.com/pythonbpf/Python-BPF/blob/main/BCC-Examples/vfsreadlat.py) - Latency measurement with `ktime()` in kprobes
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
* Explore {doc}`maps` for data storage with helpers
|
||||||
|
* Learn about {doc}`compilation` to understand helper implementation
|
||||||
|
* See {doc}`decorators` for marking BPF functions
|
||||||
87
docs/user-guide/index.md
Normal file
87
docs/user-guide/index.md
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# User Guide
|
||||||
|
|
||||||
|
This user guide provides comprehensive documentation for all PythonBPF features. Whether you're building simple tracing tools or complex performance monitoring systems, this guide will help you master PythonBPF.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
PythonBPF transforms Python code into eBPF bytecode that runs in the Linux kernel. It provides a Pythonic interface to eBPF features through decorators, type annotations, and familiar programming patterns.
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### Decorators
|
||||||
|
|
||||||
|
PythonBPF uses decorators to mark code for BPF compilation:
|
||||||
|
|
||||||
|
* `@bpf` - Mark functions and classes for BPF compilation
|
||||||
|
* `@map` - Define BPF maps for data storage
|
||||||
|
* `@struct` - Define custom data structures
|
||||||
|
* `@section(name)` - Specify attachment points
|
||||||
|
* `@bpfglobal` - Define global variables
|
||||||
|
|
||||||
|
### Compilation Pipeline
|
||||||
|
|
||||||
|
Your Python code goes through several stages:
|
||||||
|
|
||||||
|
1. **IR Generation** - The Python AST is transformed into LLVM IR using llvmlite
|
||||||
|
2. **BPF Compilation** - LLVM IR is compiled to BPF bytecode using `llc`
|
||||||
|
3. **Loading** - The BPF object is loaded into the kernel using libbpf
|
||||||
|
4. **Attachment** - Programs are attached to kernel hooks (tracepoints, kprobes, etc.)
|
||||||
|
|
||||||
|
## Code Organization
|
||||||
|
|
||||||
|
When writing BPF programs with PythonBPF, we recommend:
|
||||||
|
|
||||||
|
1. **Use type hints** - Required for proper code generation
|
||||||
|
2. **Test incrementally** - Verify each component works before adding complexity
|
||||||
|
|
||||||
|
## Type System
|
||||||
|
|
||||||
|
PythonBPF uses Python's `ctypes` module for type definitions:
|
||||||
|
|
||||||
|
* `c_int8`, `c_int16`, `c_int32`, `c_int64` - Signed integers
|
||||||
|
* `c_uint8`, `c_uint16`, `c_uint32`, `c_uint64` - Unsigned integers
|
||||||
|
* `c_char`, `c_bool` - Characters and booleans
|
||||||
|
* `c_void_p` - Void pointers
|
||||||
|
* `str(N)` - Fixed-length strings (e.g., `str(16)` for 16-byte string)
|
||||||
|
|
||||||
|
## Example Structure
|
||||||
|
|
||||||
|
A typical PythonBPF program follows this structure:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, map, section, bpfglobal, BPF, compile
|
||||||
|
from pythonbpf.maps import HashMap
|
||||||
|
from ctypes import c_void_p, c_int64, c_uint32
|
||||||
|
|
||||||
|
# Define maps
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def my_map() -> HashMap:
|
||||||
|
return HashMap(key=c_uint32, value=c_uint64, max_entries=1024)
|
||||||
|
|
||||||
|
# Define BPF function
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/...")
|
||||||
|
def my_function(ctx: c_void_p) -> c_int64:
|
||||||
|
# BPF logic here
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# License (required)
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
# Compile, load, and run
|
||||||
|
if __name__ == "__main__":
|
||||||
|
b = BPF()
|
||||||
|
b.load_and_attach()
|
||||||
|
# Use the program...
|
||||||
|
|
||||||
|
# Or, compile to an object file
|
||||||
|
compile()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Start with {doc}`decorators` to learn about all available decorators, then explore the other sections to master specific features.
|
||||||
476
docs/user-guide/maps.md
Normal file
476
docs/user-guide/maps.md
Normal file
@ -0,0 +1,476 @@
|
|||||||
|
# BPF Maps
|
||||||
|
|
||||||
|
Maps are BPF data structures that provide storage and communication mechanisms. They allow BPF programs to:
|
||||||
|
|
||||||
|
* Store state between invocations
|
||||||
|
* Share data between multiple BPF programs
|
||||||
|
* Communicate with userspace applications
|
||||||
|
|
||||||
|
```{note}
|
||||||
|
**Work in Progress:** PythonBPF is under active development. We are constantly adding support for more map types, helpers, and kfuncs. Check back for updates!
|
||||||
|
```
|
||||||
|
For comprehensive documentation on BPF maps, see the [eBPF Maps documentation on ebpf.io](https://ebpf.io/what-is-ebpf/#maps).
|
||||||
|
|
||||||
|
## Map Types
|
||||||
|
|
||||||
|
PythonBPF supports several map types, each optimized for different use cases.
|
||||||
|
|
||||||
|
### HashMap
|
||||||
|
|
||||||
|
Hash maps provide efficient key-value storage with O(1) lookup time.
|
||||||
|
|
||||||
|
> **Linux Kernel Map Type:** `BPF_MAP_TYPE_HASH`
|
||||||
|
|
||||||
|
#### Definition
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, map
|
||||||
|
from pythonbpf.maps import HashMap
|
||||||
|
from ctypes import c_uint32, c_uint64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def my_map() -> HashMap:
|
||||||
|
return HashMap(
|
||||||
|
key=c_uint32,
|
||||||
|
value=c_uint64,
|
||||||
|
max_entries=1024
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
* `key` - The type of the key (must be a ctypes type or struct)
|
||||||
|
* `value` - The type of the value (must be a ctypes type or struct)
|
||||||
|
* `max_entries` - Maximum number of entries the map can hold
|
||||||
|
|
||||||
|
#### Operations
|
||||||
|
|
||||||
|
##### lookup(key)
|
||||||
|
|
||||||
|
Look up a value by key. Returns the value if found, `None` otherwise.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_open")
|
||||||
|
def trace_open(ctx: c_void_p) -> c_int64:
|
||||||
|
value = my_map.lookup(1)
|
||||||
|
if value:
|
||||||
|
print(f"Found value: {value}")
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
##### update(key, value, flags=None)
|
||||||
|
|
||||||
|
Update or insert a key-value pair.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_open")
|
||||||
|
def track_opens(ctx: c_void_p) -> c_int64:
|
||||||
|
key = pid()
|
||||||
|
count = my_map.lookup(key)
|
||||||
|
if count:
|
||||||
|
my_map.update(key, count + 1)
|
||||||
|
else:
|
||||||
|
my_map.update(key, 1)
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
##### delete(key)
|
||||||
|
|
||||||
|
Remove an entry from the map.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf
|
||||||
|
def cleanup(ctx: c_void_p) -> c_int64:
|
||||||
|
my_map.delete(1)
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Use Cases
|
||||||
|
|
||||||
|
* Counting events per process/CPU
|
||||||
|
* Storing timestamps for latency calculations
|
||||||
|
* Caching lookup results
|
||||||
|
* Implementing rate limiters
|
||||||
|
|
||||||
|
#### Example: Process Counter
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, map, section, bpfglobal, BPF
|
||||||
|
from pythonbpf.maps import HashMap
|
||||||
|
from pythonbpf.helper import pid
|
||||||
|
from ctypes import c_void_p, c_int64, c_uint32, c_uint64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def process_count() -> HashMap:
|
||||||
|
return HashMap(key=c_uint32, value=c_uint64, max_entries=4096)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_clone")
|
||||||
|
def count_processes(ctx: c_void_p) -> c_int64:
|
||||||
|
process_id = pid()
|
||||||
|
count = process_count.lookup(process_id)
|
||||||
|
|
||||||
|
if count:
|
||||||
|
new_count = count + 1
|
||||||
|
process_count.update(process_id, new_count)
|
||||||
|
else:
|
||||||
|
process_count.update(process_id, 1)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
b = BPF()
|
||||||
|
b.load_and_attach()
|
||||||
|
# Access map from userspace
|
||||||
|
from pylibbpf import BpfMap
|
||||||
|
map_obj = BpfMap(b, process_count)
|
||||||
|
# Read values...
|
||||||
|
```
|
||||||
|
|
||||||
|
### PerfEventArray
|
||||||
|
|
||||||
|
Perf event arrays are used to send data from BPF programs to userspace with high throughput.
|
||||||
|
|
||||||
|
> **Linux Kernel Map Type:** `BPF_MAP_TYPE_PERF_EVENT_ARRAY`
|
||||||
|
|
||||||
|
#### Definition
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf.maps import PerfEventArray
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def events() -> PerfEventArray:
|
||||||
|
return PerfEventArray(
|
||||||
|
key_size=c_uint32,
|
||||||
|
value_size=c_uint32
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
* `key_size` - Type for the key (typically `c_uint32`)
|
||||||
|
* `value_size` - Type for the value (typically `c_uint32`)
|
||||||
|
|
||||||
|
#### Operations
|
||||||
|
|
||||||
|
##### output(data)
|
||||||
|
|
||||||
|
Send data to userspace. The data can be a struct or basic type.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class Event:
|
||||||
|
pid: c_uint32
|
||||||
|
timestamp: c_uint64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def events() -> PerfEventArray:
|
||||||
|
return PerfEventArray(key_size=c_uint32, value_size=c_uint32)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_execve")
|
||||||
|
def send_event(ctx: c_void_p) -> c_int64:
|
||||||
|
event = Event()
|
||||||
|
event.pid = pid()
|
||||||
|
event.timestamp = ktime()
|
||||||
|
events.output(event)
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Use Cases
|
||||||
|
|
||||||
|
* Sending detailed event data to userspace
|
||||||
|
* Real-time monitoring and alerting
|
||||||
|
* Collecting samples for analysis
|
||||||
|
* High-throughput data collection
|
||||||
|
|
||||||
|
#### Example: Event Logging
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, map, struct, section, bpfglobal, BPF
|
||||||
|
from pythonbpf.maps import PerfEventArray
|
||||||
|
from pythonbpf.helper import pid, ktime, comm
|
||||||
|
from ctypes import c_void_p, c_int64, c_uint32, c_uint64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class ProcessEvent:
|
||||||
|
timestamp: c_uint64
|
||||||
|
pid: c_uint32
|
||||||
|
comm: str(16)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def events() -> PerfEventArray:
|
||||||
|
return PerfEventArray(key_size=c_uint32, value_size=c_uint32)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_execve")
|
||||||
|
def log_exec(ctx: c_void_p) -> c_int64:
|
||||||
|
event = ProcessEvent()
|
||||||
|
event.timestamp = ktime()
|
||||||
|
event.pid = pid()
|
||||||
|
comm(event.comm) # Fills event.comm with process name
|
||||||
|
events.output(event)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
```
|
||||||
|
|
||||||
|
### RingBuffer
|
||||||
|
|
||||||
|
Ring buffers provide efficient, ordered event delivery with lower overhead than perf event arrays.
|
||||||
|
|
||||||
|
> **Linux Kernel Map Type:** `BPF_MAP_TYPE_RINGBUF`
|
||||||
|
|
||||||
|
#### Definition
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf.maps import RingBuffer
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def events() -> RingBuffer:
|
||||||
|
return RingBuffer(max_entries=4096)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
* `max_entries` - Maximum size of the ring buffer in bytes (must be power of 2)
|
||||||
|
|
||||||
|
#### Operations
|
||||||
|
|
||||||
|
##### output(data, flags=0)
|
||||||
|
|
||||||
|
Send data to the ring buffer.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_open")
|
||||||
|
def log_event(ctx: c_void_p) -> c_int64:
|
||||||
|
event = Event()
|
||||||
|
event.pid = pid()
|
||||||
|
events.output(event)
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
##### reserve(size)
|
||||||
|
|
||||||
|
Reserve space in the ring buffer. Returns a pointer to the reserved space or 0 if no space available.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf
|
||||||
|
def reserve_space(ctx: c_void_p) -> c_int64:
|
||||||
|
ptr = events.reserve(64) # Reserve 64 bytes
|
||||||
|
if ptr:
|
||||||
|
# Use the reserved space
|
||||||
|
events.submit(ptr)
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
##### submit(data, flags=0)
|
||||||
|
|
||||||
|
Submit previously reserved space.
|
||||||
|
|
||||||
|
##### discard(data, flags=0)
|
||||||
|
|
||||||
|
Discard previously reserved space without submitting.
|
||||||
|
|
||||||
|
#### Use Cases
|
||||||
|
|
||||||
|
* Modern event streaming (preferred over PerfEventArray)
|
||||||
|
* Lower overhead event delivery
|
||||||
|
* Ordered event processing
|
||||||
|
* Kernel 5.8+ systems
|
||||||
|
|
||||||
|
#### Advantages over PerfEventArray
|
||||||
|
|
||||||
|
* Lower memory overhead
|
||||||
|
* Better performance
|
||||||
|
* Simpler API
|
||||||
|
* Ordered delivery guarantees
|
||||||
|
|
||||||
|
### BPFMapType Enum
|
||||||
|
|
||||||
|
PythonBPF supports various BPF map types through the `BPFMapType` enum:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf.maps import BPFMapType
|
||||||
|
|
||||||
|
# Common map types
|
||||||
|
BPFMapType.BPF_MAP_TYPE_HASH # Hash map
|
||||||
|
BPFMapType.BPF_MAP_TYPE_ARRAY # Array map
|
||||||
|
BPFMapType.BPF_MAP_TYPE_PERF_EVENT_ARRAY # Perf event array
|
||||||
|
BPFMapType.BPF_MAP_TYPE_RINGBUF # Ring buffer
|
||||||
|
BPFMapType.BPF_MAP_TYPE_STACK_TRACE # Stack trace storage
|
||||||
|
BPFMapType.BPF_MAP_TYPE_LRU_HASH # LRU hash map
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using Maps with Structs
|
||||||
|
|
||||||
|
Maps can store complex data types using structs as values:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, map, struct, section
|
||||||
|
from pythonbpf.maps import HashMap
|
||||||
|
from ctypes import c_uint32, c_uint64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class Stats:
|
||||||
|
count: c_uint64
|
||||||
|
total_time: c_uint64
|
||||||
|
max_time: c_uint64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def process_stats() -> HashMap:
|
||||||
|
return HashMap(
|
||||||
|
key=c_uint32, # PID as key
|
||||||
|
value=Stats, # Struct as value
|
||||||
|
max_entries=1024
|
||||||
|
)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_read")
|
||||||
|
def track_stats(ctx: c_void_p) -> c_int64:
|
||||||
|
process_id = pid()
|
||||||
|
stats = process_stats.lookup(process_id)
|
||||||
|
|
||||||
|
if stats:
|
||||||
|
stats.count = stats.count + 1
|
||||||
|
process_stats.update(process_id, stats)
|
||||||
|
else:
|
||||||
|
new_stats = Stats()
|
||||||
|
new_stats.count = 1
|
||||||
|
new_stats.total_time = 0
|
||||||
|
new_stats.max_time = 0
|
||||||
|
process_stats.update(process_id, new_stats)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accessing Maps from Userspace
|
||||||
|
|
||||||
|
After loading a BPF program, you can access maps from Python using `pylibbpf`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import BPF
|
||||||
|
from pylibbpf import BpfMap
|
||||||
|
|
||||||
|
# Load BPF program
|
||||||
|
b = BPF()
|
||||||
|
b.load_and_attach()
|
||||||
|
|
||||||
|
# Get map reference
|
||||||
|
map_obj = BpfMap(b, my_map)
|
||||||
|
|
||||||
|
# Read all key-value pairs
|
||||||
|
for key, value in map_obj.items():
|
||||||
|
print(f"Key: {key}, Value: {value}")
|
||||||
|
|
||||||
|
# Get all keys
|
||||||
|
keys = list(map_obj.keys())
|
||||||
|
|
||||||
|
# Get all values
|
||||||
|
values = list(map_obj.values())
|
||||||
|
|
||||||
|
# Lookup specific key
|
||||||
|
value = map_obj[key]
|
||||||
|
|
||||||
|
# Update from userspace
|
||||||
|
map_obj[key] = new_value
|
||||||
|
|
||||||
|
# Delete from userspace
|
||||||
|
del map_obj[key]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Counter Pattern
|
||||||
|
|
||||||
|
```python
|
||||||
|
count = my_map.lookup(key)
|
||||||
|
if count:
|
||||||
|
my_map.update(key, count + 1)
|
||||||
|
else:
|
||||||
|
my_map.update(key, 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Latency Tracking
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Store start time
|
||||||
|
start = ktime()
|
||||||
|
start_map.update(key, start)
|
||||||
|
|
||||||
|
# Later: calculate latency
|
||||||
|
start_time = start_map.lookup(key)
|
||||||
|
if start_time:
|
||||||
|
latency = ktime() - start_time
|
||||||
|
latency_map.update(key, latency)
|
||||||
|
start_map.delete(key)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Sampling
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Only process every Nth event
|
||||||
|
count = counter.lookup(key)
|
||||||
|
if count and (count % 100) == 0:
|
||||||
|
events.output(data)
|
||||||
|
counter.update(key, count + 1 if count else 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Map Not Found
|
||||||
|
|
||||||
|
If you get "map not found" errors:
|
||||||
|
* Ensure the map is defined with `@bpf` and `@map`
|
||||||
|
* Check that the map name matches exactly
|
||||||
|
* Verify the BPF program loaded successfully
|
||||||
|
|
||||||
|
### Map Full
|
||||||
|
|
||||||
|
If updates fail due to map being full:
|
||||||
|
* Increase `max_entries`
|
||||||
|
* Use LRU maps for automatic eviction
|
||||||
|
* Add cleanup logic to delete old entries
|
||||||
|
|
||||||
|
### Type Errors
|
||||||
|
|
||||||
|
If you get type-related errors:
|
||||||
|
* Verify key and value types match the definition
|
||||||
|
* Check that structs are properly defined
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Check out these examples in the `BCC-Examples/` directory that demonstrate map usage:
|
||||||
|
|
||||||
|
* [sync_timing.py](https://github.com/pythonbpf/Python-BPF/blob/main/BCC-Examples/sync_timing.py) - HashMap for storing timestamps
|
||||||
|
* [sync_count.py](https://github.com/pythonbpf/Python-BPF/blob/main/BCC-Examples/sync_count.py) - HashMap for counting events
|
||||||
|
* [hello_perf_output.py](https://github.com/pythonbpf/Python-BPF/blob/main/BCC-Examples/hello_perf_output.py) - PerfEventArray for sending structs to userspace
|
||||||
|
* [sync_perf_output.py](https://github.com/pythonbpf/Python-BPF/blob/main/BCC-Examples/sync_perf_output.py) - PerfEventArray with timing data
|
||||||
|
* [disksnoop.py](https://github.com/pythonbpf/Python-BPF/blob/main/BCC-Examples/disksnoop.py) - HashMap for tracking disk I/O
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
* Learn about {doc}`structs` for defining custom value types
|
||||||
|
* Explore {doc}`helpers` for BPF helper functions
|
||||||
|
* See {doc}`compilation` to understand how maps are compiled
|
||||||
413
docs/user-guide/structs.md
Normal file
413
docs/user-guide/structs.md
Normal file
@ -0,0 +1,413 @@
|
|||||||
|
# BPF Structs
|
||||||
|
|
||||||
|
Structs allow you to define custom data types for use in BPF programs. They provide a way to group related fields together and can be used as map values, event payloads, or local variables.
|
||||||
|
|
||||||
|
## Defining Structs
|
||||||
|
|
||||||
|
Use the `@bpf` and `@struct` decorators to define a BPF struct:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, struct
|
||||||
|
from ctypes import c_uint64, c_uint32
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class Event:
|
||||||
|
timestamp: c_uint64
|
||||||
|
pid: c_uint32
|
||||||
|
cpu: c_uint32
|
||||||
|
```
|
||||||
|
|
||||||
|
## Field Types
|
||||||
|
|
||||||
|
Structs support various field types from Python's `ctypes` module.
|
||||||
|
|
||||||
|
### Integer Types
|
||||||
|
|
||||||
|
```python
|
||||||
|
from ctypes import (
|
||||||
|
c_int8, c_int16, c_int32, c_int64,
|
||||||
|
c_uint8, c_uint16, c_uint32, c_uint64
|
||||||
|
)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class Numbers:
|
||||||
|
small_int: c_int8 # -128 to 127
|
||||||
|
short_int: c_int16 # -32768 to 32767
|
||||||
|
int_val: c_int32 # -2^31 to 2^31-1
|
||||||
|
long_int: c_int64 # -2^63 to 2^63-1
|
||||||
|
|
||||||
|
byte: c_uint8 # 0 to 255
|
||||||
|
word: c_uint16 # 0 to 65535
|
||||||
|
dword: c_uint32 # 0 to 2^32-1
|
||||||
|
qword: c_uint64 # 0 to 2^64-1
|
||||||
|
```
|
||||||
|
|
||||||
|
### String Types
|
||||||
|
|
||||||
|
Fixed-length strings are defined using `str(N)` where N is the size:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class ProcessInfo:
|
||||||
|
name: str(16) # 16-byte string
|
||||||
|
path: str(256) # 256-byte string
|
||||||
|
```
|
||||||
|
|
||||||
|
```{note}
|
||||||
|
Strings in BPF are fixed-length and null-terminated. The size includes the null terminator.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pointer Types
|
||||||
|
|
||||||
|
```python
|
||||||
|
from ctypes import c_void_p, c_char_p
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class Pointers:
|
||||||
|
ptr: c_void_p # Generic pointer
|
||||||
|
str_ptr: c_char_p # Character pointer
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nested Structs
|
||||||
|
|
||||||
|
Structs can contain other structs as fields:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class Address:
|
||||||
|
street: str(64)
|
||||||
|
city: str(32)
|
||||||
|
zip_code: c_uint32
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class Person:
|
||||||
|
name: str(32)
|
||||||
|
age: c_uint32
|
||||||
|
address: Address # Nested struct
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using Structs
|
||||||
|
|
||||||
|
### As Local Variables
|
||||||
|
|
||||||
|
Create and use struct instances within BPF functions:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, struct, section
|
||||||
|
from pythonbpf.helper import pid, ktime, comm
|
||||||
|
from ctypes import c_void_p, c_int64, c_uint64, c_uint32
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class Event:
|
||||||
|
timestamp: c_uint64
|
||||||
|
pid: c_uint32
|
||||||
|
comm: str(16)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_execve")
|
||||||
|
def capture_event(ctx: c_void_p) -> c_int64:
|
||||||
|
# Create an instance
|
||||||
|
event = Event()
|
||||||
|
|
||||||
|
# Set fields
|
||||||
|
event.timestamp = ktime()
|
||||||
|
event.pid = pid()
|
||||||
|
comm(event.comm) # Fills event.comm with process name
|
||||||
|
|
||||||
|
# Use the struct
|
||||||
|
print(f"Process with PID {event.pid}")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### As Map Keys and Values
|
||||||
|
|
||||||
|
Use structs as keys and values in maps for complex state storage:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, struct, map, section
|
||||||
|
from pythonbpf.maps import HashMap
|
||||||
|
from ctypes import c_uint32, c_uint64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class ProcessStats:
|
||||||
|
syscall_count: c_uint64
|
||||||
|
total_time: c_uint64
|
||||||
|
max_latency: c_uint64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def stats() -> HashMap:
|
||||||
|
return HashMap(
|
||||||
|
key=c_uint32,
|
||||||
|
value=ProcessStats,
|
||||||
|
max_entries=1024
|
||||||
|
)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_read")
|
||||||
|
def track_syscalls(ctx: c_void_p) -> c_int64:
|
||||||
|
process_id = pid()
|
||||||
|
|
||||||
|
# Lookup existing stats
|
||||||
|
s = stats.lookup(process_id)
|
||||||
|
|
||||||
|
if s:
|
||||||
|
# Update existing stats
|
||||||
|
s.syscall_count = s.syscall_count + 1
|
||||||
|
stats.update(process_id, s)
|
||||||
|
else:
|
||||||
|
# Create new stats
|
||||||
|
new_stats = ProcessStats()
|
||||||
|
new_stats.syscall_count = 1
|
||||||
|
new_stats.total_time = 0
|
||||||
|
new_stats.max_latency = 0
|
||||||
|
stats.update(process_id, new_stats)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Perf Events
|
||||||
|
|
||||||
|
Send struct data to userspace using PerfEventArray:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, struct, map, section
|
||||||
|
from pythonbpf.maps import PerfEventArray
|
||||||
|
from pythonbpf.helper import pid, ktime, comm
|
||||||
|
from ctypes import c_void_p, c_int64, c_uint32, c_uint64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class ProcessEvent:
|
||||||
|
timestamp: c_uint64
|
||||||
|
pid: c_uint32
|
||||||
|
ppid: c_uint32
|
||||||
|
comm: str(16)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def events() -> PerfEventArray:
|
||||||
|
return PerfEventArray(key_size=c_uint32, value_size=c_uint32)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/sched/sched_process_fork")
|
||||||
|
def trace_fork(ctx: c_void_p) -> c_int64:
|
||||||
|
event = ProcessEvent()
|
||||||
|
event.timestamp = ktime()
|
||||||
|
event.pid = pid()
|
||||||
|
comm(event.comm) # Fills event.comm with process name
|
||||||
|
|
||||||
|
# Send to userspace
|
||||||
|
events.output(event)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Ring Buffers
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, struct, map, section
|
||||||
|
from pythonbpf.maps import RingBuffer
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class FileEvent:
|
||||||
|
timestamp: c_uint64
|
||||||
|
pid: c_uint32
|
||||||
|
filename: str(256)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def events() -> RingBuffer:
|
||||||
|
return RingBuffer(max_entries=4096)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/syscalls/sys_enter_openat")
|
||||||
|
def trace_open(ctx: c_void_p) -> c_int64:
|
||||||
|
event = FileEvent()
|
||||||
|
event.timestamp = ktime()
|
||||||
|
event.pid = pid()
|
||||||
|
|
||||||
|
events.output(event)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Field Access and Modification
|
||||||
|
|
||||||
|
### Reading Fields
|
||||||
|
|
||||||
|
Access struct fields using dot notation:
|
||||||
|
|
||||||
|
```python
|
||||||
|
event = Event()
|
||||||
|
ts = event.timestamp
|
||||||
|
process_id = event.pid
|
||||||
|
```
|
||||||
|
|
||||||
|
### Writing Fields
|
||||||
|
|
||||||
|
Assign values to fields:
|
||||||
|
|
||||||
|
```python
|
||||||
|
event = Event()
|
||||||
|
event.timestamp = ktime()
|
||||||
|
event.pid = pid()
|
||||||
|
comm(event.comm)
|
||||||
|
```
|
||||||
|
|
||||||
|
## StructType Class
|
||||||
|
|
||||||
|
PythonBPF provides a `StructType` class for working with struct metadata:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf.structs import StructType
|
||||||
|
|
||||||
|
# Define a struct
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class MyStruct:
|
||||||
|
field1: c_uint64
|
||||||
|
field2: c_uint32
|
||||||
|
|
||||||
|
# Access struct information (from userspace)
|
||||||
|
# This is typically used internally by the compiler
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complex Examples
|
||||||
|
|
||||||
|
### Network Packet Event
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pythonbpf import bpf, struct, map, section
|
||||||
|
from pythonbpf.maps import RingBuffer
|
||||||
|
from pythonbpf.helper import ktime, XDP_PASS
|
||||||
|
from ctypes import c_void_p, c_int64, c_uint8, c_uint16, c_uint32, c_uint64
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class PacketEvent:
|
||||||
|
timestamp: c_uint64
|
||||||
|
src_ip: c_uint32
|
||||||
|
dst_ip: c_uint32
|
||||||
|
src_port: c_uint16
|
||||||
|
dst_port: c_uint16
|
||||||
|
protocol: c_uint8
|
||||||
|
length: c_uint16
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def packets() -> RingBuffer:
|
||||||
|
return RingBuffer(max_entries=8192)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("xdp")
|
||||||
|
def capture_packets(ctx: c_void_p) -> c_int64:
|
||||||
|
pkt = PacketEvent()
|
||||||
|
pkt.timestamp = ktime()
|
||||||
|
# Parse packet data from ctx...
|
||||||
|
|
||||||
|
packets.output(pkt)
|
||||||
|
|
||||||
|
return XDP_PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
### Process Lifecycle Tracking
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class ProcessLifecycle:
|
||||||
|
pid: c_uint32
|
||||||
|
ppid: c_uint32
|
||||||
|
start_time: c_uint64
|
||||||
|
exit_time: c_uint64
|
||||||
|
exit_code: c_int32
|
||||||
|
comm: str(16)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def process_info() -> HashMap:
|
||||||
|
return HashMap(
|
||||||
|
key=c_uint32,
|
||||||
|
value=ProcessLifecycle,
|
||||||
|
max_entries=4096
|
||||||
|
)
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/sched/sched_process_fork")
|
||||||
|
def track_fork(ctx: c_void_p) -> c_int64:
|
||||||
|
process_id = pid()
|
||||||
|
|
||||||
|
info = ProcessLifecycle()
|
||||||
|
info.pid = process_id
|
||||||
|
info.start_time = ktime()
|
||||||
|
|
||||||
|
process_info.update(process_id, info)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/sched/sched_process_exit")
|
||||||
|
def track_exit(ctx: c_void_p) -> c_int64:
|
||||||
|
process_id = pid()
|
||||||
|
|
||||||
|
info = process_info.lookup(process_id)
|
||||||
|
if info:
|
||||||
|
info.exit_time = ktime()
|
||||||
|
process_info.update(process_id, info)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Struct Size Issues
|
||||||
|
|
||||||
|
If you encounter size-related errors:
|
||||||
|
* Check for excessive padding
|
||||||
|
* Verify field types are correct
|
||||||
|
* Consider reordering fields
|
||||||
|
|
||||||
|
### Initialization Problems
|
||||||
|
|
||||||
|
If fields aren't initialized correctly:
|
||||||
|
* Always initialize all fields explicitly
|
||||||
|
* Set default values where appropriate
|
||||||
|
* Use helper functions for dynamic values
|
||||||
|
|
||||||
|
### Type Mismatch Errors
|
||||||
|
|
||||||
|
If you get type errors:
|
||||||
|
* Ensure field types match assignments
|
||||||
|
* Check that imported types are from `ctypes`
|
||||||
|
* Verify nested struct definitions
|
||||||
|
|
||||||
|
## Reading Struct Data in Userspace
|
||||||
|
|
||||||
|
After capturing struct data, read it in Python:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pylibbpf import BpfMap
|
||||||
|
|
||||||
|
# Read from map
|
||||||
|
map_obj = BpfMap(b, stats)
|
||||||
|
for key, value_bytes in map_obj.items():
|
||||||
|
value = Event.from_buffer_copy(value_bytes)
|
||||||
|
print(f"PID: {value.pid}, Comm: {value.comm.decode()}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
* Learn about {doc}`maps` for storing struct data
|
||||||
|
* Explore {doc}`helpers` for populating struct fields
|
||||||
|
* See {doc}`compilation` to understand how structs are compiled
|
||||||
File diff suppressed because one or more lines are too long
22
examples/anomaly-detection/lib/__init__.py
Normal file
22
examples/anomaly-detection/lib/__init__.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
"""
|
||||||
|
Process Anomaly Detection - Constants and Utilities
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
MAX_SYSCALLS = 548
|
||||||
|
|
||||||
|
|
||||||
|
def comm_for_pid(pid: int) -> bytes | None:
|
||||||
|
"""Get process name from /proc."""
|
||||||
|
try:
|
||||||
|
with open(f"/proc/{pid}/comm", "rb") as f:
|
||||||
|
return f.read().strip()
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.warning(f"Process with PID {pid} not found.")
|
||||||
|
except PermissionError:
|
||||||
|
logger.warning(f"Permission denied when accessing /proc/{pid}/comm.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error reading /proc/{pid}/comm: {e}")
|
||||||
|
return None
|
||||||
173
examples/anomaly-detection/lib/ml.py
Normal file
173
examples/anomaly-detection/lib/ml.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
"""
|
||||||
|
Autoencoder for Process Behavior Anomaly Detection
|
||||||
|
|
||||||
|
Uses Keras/TensorFlow to train an autoencoder on syscall patterns.
|
||||||
|
Anomalies are detected when reconstruction error exceeds threshold.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from sklearn.model_selection import train_test_split
|
||||||
|
from tensorflow import keras
|
||||||
|
|
||||||
|
from lib import MAX_SYSCALLS
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def create_autoencoder(n_inputs: int = MAX_SYSCALLS) -> keras.Model:
|
||||||
|
"""
|
||||||
|
Create the autoencoder architecture.
|
||||||
|
|
||||||
|
Architecture: input → encoder → bottleneck → decoder → output
|
||||||
|
"""
|
||||||
|
inp = keras.Input(shape=(n_inputs,))
|
||||||
|
|
||||||
|
# Encoder
|
||||||
|
encoder = keras.layers.Dense(n_inputs)(inp)
|
||||||
|
encoder = keras.layers.ReLU()(encoder)
|
||||||
|
|
||||||
|
# Bottleneck (compressed representation)
|
||||||
|
bottleneck = keras.layers.Dense(n_inputs // 2)(encoder)
|
||||||
|
|
||||||
|
# Decoder
|
||||||
|
decoder = keras.layers.Dense(n_inputs)(bottleneck)
|
||||||
|
decoder = keras.layers.ReLU()(decoder)
|
||||||
|
output = keras.layers.Dense(n_inputs, activation="linear")(decoder)
|
||||||
|
|
||||||
|
model = keras.Model(inp, output)
|
||||||
|
model.compile(optimizer="adam", loss="mse")
|
||||||
|
|
||||||
|
return model
|
||||||
|
|
||||||
|
|
||||||
|
class AutoEncoder:
|
||||||
|
"""
|
||||||
|
Autoencoder for syscall pattern anomaly detection.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
# Training
|
||||||
|
ae = AutoEncoder('model.keras')
|
||||||
|
model, threshold = ae.train('data.csv', epochs=200)
|
||||||
|
|
||||||
|
# Inference
|
||||||
|
ae = AutoEncoder('model.keras', load=True)
|
||||||
|
_, errors, total_error = ae.predict([features])
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, filename: str, load: bool = False):
|
||||||
|
self.filename = filename
|
||||||
|
self.model = None
|
||||||
|
|
||||||
|
if load:
|
||||||
|
self._load_model()
|
||||||
|
|
||||||
|
def _load_model(self) -> None:
|
||||||
|
"""Load a trained model from disk."""
|
||||||
|
if not os.path.exists(self.filename):
|
||||||
|
raise FileNotFoundError(f"Model file not found: {self.filename}")
|
||||||
|
|
||||||
|
logger.info(f"Loading model from {self.filename}")
|
||||||
|
self.model = keras.models.load_model(self.filename)
|
||||||
|
|
||||||
|
def train(
|
||||||
|
self,
|
||||||
|
datafile: str,
|
||||||
|
epochs: int,
|
||||||
|
batch_size: int,
|
||||||
|
test_size: float = 0.1,
|
||||||
|
) -> tuple[keras.Model, float]:
|
||||||
|
"""
|
||||||
|
Train the autoencoder on collected data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
datafile: Path to CSV file with training data
|
||||||
|
epochs: Number of training epochs
|
||||||
|
batch_size: Training batch size
|
||||||
|
test_size: Fraction of data to use for validation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (trained model, error threshold)
|
||||||
|
"""
|
||||||
|
if not os.path.exists(datafile):
|
||||||
|
raise FileNotFoundError(f"Data file not found: {datafile}")
|
||||||
|
|
||||||
|
logger.info(f"Loading training data from {datafile}")
|
||||||
|
|
||||||
|
# Load and prepare data
|
||||||
|
df = pd.read_csv(datafile)
|
||||||
|
features = df.drop(["sample_time"], axis=1).values
|
||||||
|
|
||||||
|
logger.info(f"Loaded {len(features)} samples with {features.shape[1]} features")
|
||||||
|
|
||||||
|
# Split train/test
|
||||||
|
train_data, test_data = train_test_split(
|
||||||
|
features,
|
||||||
|
test_size=test_size,
|
||||||
|
random_state=42,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Training set: {len(train_data)} samples")
|
||||||
|
logger.info(f"Test set: {len(test_data)} samples")
|
||||||
|
|
||||||
|
# Create and train model
|
||||||
|
self.model = create_autoencoder()
|
||||||
|
|
||||||
|
if self.model is None:
|
||||||
|
raise RuntimeError("Failed to create the autoencoder model.")
|
||||||
|
|
||||||
|
logger.info("Training autoencoder...")
|
||||||
|
self.model.fit(
|
||||||
|
train_data,
|
||||||
|
train_data,
|
||||||
|
validation_data=(test_data, test_data),
|
||||||
|
epochs=epochs,
|
||||||
|
batch_size=batch_size,
|
||||||
|
verbose=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save model (use .keras format for Keras 3.x compatibility)
|
||||||
|
self.model.save(self.filename)
|
||||||
|
logger.info(f"Model saved to {self.filename}")
|
||||||
|
|
||||||
|
# Calculate error threshold from test data
|
||||||
|
threshold = self._calculate_threshold(test_data)
|
||||||
|
|
||||||
|
return self.model, threshold
|
||||||
|
|
||||||
|
def _calculate_threshold(self, test_data: np.ndarray) -> float:
|
||||||
|
"""Calculate error threshold from test data."""
|
||||||
|
logger.info(f"Calculating error threshold from {len(test_data)} test samples")
|
||||||
|
|
||||||
|
if self.model is None:
|
||||||
|
raise RuntimeError("Model not loaded. Use load=True or train first.")
|
||||||
|
|
||||||
|
predictions = self.model.predict(test_data, verbose=0)
|
||||||
|
errors = np.abs(test_data - predictions).sum(axis=1)
|
||||||
|
|
||||||
|
return float(errors.max())
|
||||||
|
|
||||||
|
def predict(self, X: list | np.ndarray) -> tuple[np.ndarray, np.ndarray, float]:
|
||||||
|
"""
|
||||||
|
Run prediction and return reconstruction error.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
X: Input data (list of feature vectors)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (reconstructed, per_feature_errors, total_error)
|
||||||
|
"""
|
||||||
|
if self.model is None:
|
||||||
|
raise RuntimeError("Model not loaded. Use load=True or train first.")
|
||||||
|
|
||||||
|
X = np.asarray(X, dtype=np.float32)
|
||||||
|
y = self.model.predict(X, verbose=0)
|
||||||
|
|
||||||
|
# Per-feature reconstruction error
|
||||||
|
errors = np.abs(X[0] - y[0])
|
||||||
|
total_error = float(errors.sum())
|
||||||
|
|
||||||
|
return y, errors, total_error
|
||||||
448
examples/anomaly-detection/lib/platform.py
Normal file
448
examples/anomaly-detection/lib/platform.py
Normal file
@ -0,0 +1,448 @@
|
|||||||
|
# Copyright 2017 Sasha Goldshtein
|
||||||
|
# Copyright 2018 Red Hat, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
syscall.py contains functions useful for mapping between syscall names and numbers
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Syscall table for Linux x86_64, not very recent. Automatically generated from
|
||||||
|
# https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/arch/x86/entry/syscalls/syscall_64.tbl?h=linux-6.17.y
|
||||||
|
# using the following command:
|
||||||
|
#
|
||||||
|
# cat arch/x86/entry/syscalls/syscall_64.tbl \
|
||||||
|
# | awk 'BEGIN { print "syscalls = {" }
|
||||||
|
# /^[0-9]/ { print " "$1": b\""$3"\"," }
|
||||||
|
# END { print "}" }'
|
||||||
|
|
||||||
|
SYSCALLS = {
|
||||||
|
0: b"read",
|
||||||
|
1: b"write",
|
||||||
|
2: b"open",
|
||||||
|
3: b"close",
|
||||||
|
4: b"stat",
|
||||||
|
5: b"fstat",
|
||||||
|
6: b"lstat",
|
||||||
|
7: b"poll",
|
||||||
|
8: b"lseek",
|
||||||
|
9: b"mmap",
|
||||||
|
10: b"mprotect",
|
||||||
|
11: b"munmap",
|
||||||
|
12: b"brk",
|
||||||
|
13: b"rt_sigaction",
|
||||||
|
14: b"rt_sigprocmask",
|
||||||
|
15: b"rt_sigreturn",
|
||||||
|
16: b"ioctl",
|
||||||
|
17: b"pread64",
|
||||||
|
18: b"pwrite64",
|
||||||
|
19: b"readv",
|
||||||
|
20: b"writev",
|
||||||
|
21: b"access",
|
||||||
|
22: b"pipe",
|
||||||
|
23: b"select",
|
||||||
|
24: b"sched_yield",
|
||||||
|
25: b"mremap",
|
||||||
|
26: b"msync",
|
||||||
|
27: b"mincore",
|
||||||
|
28: b"madvise",
|
||||||
|
29: b"shmget",
|
||||||
|
30: b"shmat",
|
||||||
|
31: b"shmctl",
|
||||||
|
32: b"dup",
|
||||||
|
33: b"dup2",
|
||||||
|
34: b"pause",
|
||||||
|
35: b"nanosleep",
|
||||||
|
36: b"getitimer",
|
||||||
|
37: b"alarm",
|
||||||
|
38: b"setitimer",
|
||||||
|
39: b"getpid",
|
||||||
|
40: b"sendfile",
|
||||||
|
41: b"socket",
|
||||||
|
42: b"connect",
|
||||||
|
43: b"accept",
|
||||||
|
44: b"sendto",
|
||||||
|
45: b"recvfrom",
|
||||||
|
46: b"sendmsg",
|
||||||
|
47: b"recvmsg",
|
||||||
|
48: b"shutdown",
|
||||||
|
49: b"bind",
|
||||||
|
50: b"listen",
|
||||||
|
51: b"getsockname",
|
||||||
|
52: b"getpeername",
|
||||||
|
53: b"socketpair",
|
||||||
|
54: b"setsockopt",
|
||||||
|
55: b"getsockopt",
|
||||||
|
56: b"clone",
|
||||||
|
57: b"fork",
|
||||||
|
58: b"vfork",
|
||||||
|
59: b"execve",
|
||||||
|
60: b"exit",
|
||||||
|
61: b"wait4",
|
||||||
|
62: b"kill",
|
||||||
|
63: b"uname",
|
||||||
|
64: b"semget",
|
||||||
|
65: b"semop",
|
||||||
|
66: b"semctl",
|
||||||
|
67: b"shmdt",
|
||||||
|
68: b"msgget",
|
||||||
|
69: b"msgsnd",
|
||||||
|
70: b"msgrcv",
|
||||||
|
71: b"msgctl",
|
||||||
|
72: b"fcntl",
|
||||||
|
73: b"flock",
|
||||||
|
74: b"fsync",
|
||||||
|
75: b"fdatasync",
|
||||||
|
76: b"truncate",
|
||||||
|
77: b"ftruncate",
|
||||||
|
78: b"getdents",
|
||||||
|
79: b"getcwd",
|
||||||
|
80: b"chdir",
|
||||||
|
81: b"fchdir",
|
||||||
|
82: b"rename",
|
||||||
|
83: b"mkdir",
|
||||||
|
84: b"rmdir",
|
||||||
|
85: b"creat",
|
||||||
|
86: b"link",
|
||||||
|
87: b"unlink",
|
||||||
|
88: b"symlink",
|
||||||
|
89: b"readlink",
|
||||||
|
90: b"chmod",
|
||||||
|
91: b"fchmod",
|
||||||
|
92: b"chown",
|
||||||
|
93: b"fchown",
|
||||||
|
94: b"lchown",
|
||||||
|
95: b"umask",
|
||||||
|
96: b"gettimeofday",
|
||||||
|
97: b"getrlimit",
|
||||||
|
98: b"getrusage",
|
||||||
|
99: b"sysinfo",
|
||||||
|
100: b"times",
|
||||||
|
101: b"ptrace",
|
||||||
|
102: b"getuid",
|
||||||
|
103: b"syslog",
|
||||||
|
104: b"getgid",
|
||||||
|
105: b"setuid",
|
||||||
|
106: b"setgid",
|
||||||
|
107: b"geteuid",
|
||||||
|
108: b"getegid",
|
||||||
|
109: b"setpgid",
|
||||||
|
110: b"getppid",
|
||||||
|
111: b"getpgrp",
|
||||||
|
112: b"setsid",
|
||||||
|
113: b"setreuid",
|
||||||
|
114: b"setregid",
|
||||||
|
115: b"getgroups",
|
||||||
|
116: b"setgroups",
|
||||||
|
117: b"setresuid",
|
||||||
|
118: b"getresuid",
|
||||||
|
119: b"setresgid",
|
||||||
|
120: b"getresgid",
|
||||||
|
121: b"getpgid",
|
||||||
|
122: b"setfsuid",
|
||||||
|
123: b"setfsgid",
|
||||||
|
124: b"getsid",
|
||||||
|
125: b"capget",
|
||||||
|
126: b"capset",
|
||||||
|
127: b"rt_sigpending",
|
||||||
|
128: b"rt_sigtimedwait",
|
||||||
|
129: b"rt_sigqueueinfo",
|
||||||
|
130: b"rt_sigsuspend",
|
||||||
|
131: b"sigaltstack",
|
||||||
|
132: b"utime",
|
||||||
|
133: b"mknod",
|
||||||
|
134: b"uselib",
|
||||||
|
135: b"personality",
|
||||||
|
136: b"ustat",
|
||||||
|
137: b"statfs",
|
||||||
|
138: b"fstatfs",
|
||||||
|
139: b"sysfs",
|
||||||
|
140: b"getpriority",
|
||||||
|
141: b"setpriority",
|
||||||
|
142: b"sched_setparam",
|
||||||
|
143: b"sched_getparam",
|
||||||
|
144: b"sched_setscheduler",
|
||||||
|
145: b"sched_getscheduler",
|
||||||
|
146: b"sched_get_priority_max",
|
||||||
|
147: b"sched_get_priority_min",
|
||||||
|
148: b"sched_rr_get_interval",
|
||||||
|
149: b"mlock",
|
||||||
|
150: b"munlock",
|
||||||
|
151: b"mlockall",
|
||||||
|
152: b"munlockall",
|
||||||
|
153: b"vhangup",
|
||||||
|
154: b"modify_ldt",
|
||||||
|
155: b"pivot_root",
|
||||||
|
156: b"_sysctl",
|
||||||
|
157: b"prctl",
|
||||||
|
158: b"arch_prctl",
|
||||||
|
159: b"adjtimex",
|
||||||
|
160: b"setrlimit",
|
||||||
|
161: b"chroot",
|
||||||
|
162: b"sync",
|
||||||
|
163: b"acct",
|
||||||
|
164: b"settimeofday",
|
||||||
|
165: b"mount",
|
||||||
|
166: b"umount2",
|
||||||
|
167: b"swapon",
|
||||||
|
168: b"swapoff",
|
||||||
|
169: b"reboot",
|
||||||
|
170: b"sethostname",
|
||||||
|
171: b"setdomainname",
|
||||||
|
172: b"iopl",
|
||||||
|
173: b"ioperm",
|
||||||
|
174: b"create_module",
|
||||||
|
175: b"init_module",
|
||||||
|
176: b"delete_module",
|
||||||
|
177: b"get_kernel_syms",
|
||||||
|
178: b"query_module",
|
||||||
|
179: b"quotactl",
|
||||||
|
180: b"nfsservctl",
|
||||||
|
181: b"getpmsg",
|
||||||
|
182: b"putpmsg",
|
||||||
|
183: b"afs_syscall",
|
||||||
|
184: b"tuxcall",
|
||||||
|
185: b"security",
|
||||||
|
186: b"gettid",
|
||||||
|
187: b"readahead",
|
||||||
|
188: b"setxattr",
|
||||||
|
189: b"lsetxattr",
|
||||||
|
190: b"fsetxattr",
|
||||||
|
191: b"getxattr",
|
||||||
|
192: b"lgetxattr",
|
||||||
|
193: b"fgetxattr",
|
||||||
|
194: b"listxattr",
|
||||||
|
195: b"llistxattr",
|
||||||
|
196: b"flistxattr",
|
||||||
|
197: b"removexattr",
|
||||||
|
198: b"lremovexattr",
|
||||||
|
199: b"fremovexattr",
|
||||||
|
200: b"tkill",
|
||||||
|
201: b"time",
|
||||||
|
202: b"futex",
|
||||||
|
203: b"sched_setaffinity",
|
||||||
|
204: b"sched_getaffinity",
|
||||||
|
205: b"set_thread_area",
|
||||||
|
206: b"io_setup",
|
||||||
|
207: b"io_destroy",
|
||||||
|
208: b"io_getevents",
|
||||||
|
209: b"io_submit",
|
||||||
|
210: b"io_cancel",
|
||||||
|
211: b"get_thread_area",
|
||||||
|
212: b"lookup_dcookie",
|
||||||
|
213: b"epoll_create",
|
||||||
|
214: b"epoll_ctl_old",
|
||||||
|
215: b"epoll_wait_old",
|
||||||
|
216: b"remap_file_pages",
|
||||||
|
217: b"getdents64",
|
||||||
|
218: b"set_tid_address",
|
||||||
|
219: b"restart_syscall",
|
||||||
|
220: b"semtimedop",
|
||||||
|
221: b"fadvise64",
|
||||||
|
222: b"timer_create",
|
||||||
|
223: b"timer_settime",
|
||||||
|
224: b"timer_gettime",
|
||||||
|
225: b"timer_getoverrun",
|
||||||
|
226: b"timer_delete",
|
||||||
|
227: b"clock_settime",
|
||||||
|
228: b"clock_gettime",
|
||||||
|
229: b"clock_getres",
|
||||||
|
230: b"clock_nanosleep",
|
||||||
|
231: b"exit_group",
|
||||||
|
232: b"epoll_wait",
|
||||||
|
233: b"epoll_ctl",
|
||||||
|
234: b"tgkill",
|
||||||
|
235: b"utimes",
|
||||||
|
236: b"vserver",
|
||||||
|
237: b"mbind",
|
||||||
|
238: b"set_mempolicy",
|
||||||
|
239: b"get_mempolicy",
|
||||||
|
240: b"mq_open",
|
||||||
|
241: b"mq_unlink",
|
||||||
|
242: b"mq_timedsend",
|
||||||
|
243: b"mq_timedreceive",
|
||||||
|
244: b"mq_notify",
|
||||||
|
245: b"mq_getsetattr",
|
||||||
|
246: b"kexec_load",
|
||||||
|
247: b"waitid",
|
||||||
|
248: b"add_key",
|
||||||
|
249: b"request_key",
|
||||||
|
250: b"keyctl",
|
||||||
|
251: b"ioprio_set",
|
||||||
|
252: b"ioprio_get",
|
||||||
|
253: b"inotify_init",
|
||||||
|
254: b"inotify_add_watch",
|
||||||
|
255: b"inotify_rm_watch",
|
||||||
|
256: b"migrate_pages",
|
||||||
|
257: b"openat",
|
||||||
|
258: b"mkdirat",
|
||||||
|
259: b"mknodat",
|
||||||
|
260: b"fchownat",
|
||||||
|
261: b"futimesat",
|
||||||
|
262: b"newfstatat",
|
||||||
|
263: b"unlinkat",
|
||||||
|
264: b"renameat",
|
||||||
|
265: b"linkat",
|
||||||
|
266: b"symlinkat",
|
||||||
|
267: b"readlinkat",
|
||||||
|
268: b"fchmodat",
|
||||||
|
269: b"faccessat",
|
||||||
|
270: b"pselect6",
|
||||||
|
271: b"ppoll",
|
||||||
|
272: b"unshare",
|
||||||
|
273: b"set_robust_list",
|
||||||
|
274: b"get_robust_list",
|
||||||
|
275: b"splice",
|
||||||
|
276: b"tee",
|
||||||
|
277: b"sync_file_range",
|
||||||
|
278: b"vmsplice",
|
||||||
|
279: b"move_pages",
|
||||||
|
280: b"utimensat",
|
||||||
|
281: b"epoll_pwait",
|
||||||
|
282: b"signalfd",
|
||||||
|
283: b"timerfd_create",
|
||||||
|
284: b"eventfd",
|
||||||
|
285: b"fallocate",
|
||||||
|
286: b"timerfd_settime",
|
||||||
|
287: b"timerfd_gettime",
|
||||||
|
288: b"accept4",
|
||||||
|
289: b"signalfd4",
|
||||||
|
290: b"eventfd2",
|
||||||
|
291: b"epoll_create1",
|
||||||
|
292: b"dup3",
|
||||||
|
293: b"pipe2",
|
||||||
|
294: b"inotify_init1",
|
||||||
|
295: b"preadv",
|
||||||
|
296: b"pwritev",
|
||||||
|
297: b"rt_tgsigqueueinfo",
|
||||||
|
298: b"perf_event_open",
|
||||||
|
299: b"recvmmsg",
|
||||||
|
300: b"fanotify_init",
|
||||||
|
301: b"fanotify_mark",
|
||||||
|
302: b"prlimit64",
|
||||||
|
303: b"name_to_handle_at",
|
||||||
|
304: b"open_by_handle_at",
|
||||||
|
305: b"clock_adjtime",
|
||||||
|
306: b"syncfs",
|
||||||
|
307: b"sendmmsg",
|
||||||
|
308: b"setns",
|
||||||
|
309: b"getcpu",
|
||||||
|
310: b"process_vm_readv",
|
||||||
|
311: b"process_vm_writev",
|
||||||
|
312: b"kcmp",
|
||||||
|
313: b"finit_module",
|
||||||
|
314: b"sched_setattr",
|
||||||
|
315: b"sched_getattr",
|
||||||
|
316: b"renameat2",
|
||||||
|
317: b"seccomp",
|
||||||
|
318: b"getrandom",
|
||||||
|
319: b"memfd_create",
|
||||||
|
320: b"kexec_file_load",
|
||||||
|
321: b"bpf",
|
||||||
|
322: b"execveat",
|
||||||
|
323: b"userfaultfd",
|
||||||
|
324: b"membarrier",
|
||||||
|
325: b"mlock2",
|
||||||
|
326: b"copy_file_range",
|
||||||
|
327: b"preadv2",
|
||||||
|
328: b"pwritev2",
|
||||||
|
329: b"pkey_mprotect",
|
||||||
|
330: b"pkey_alloc",
|
||||||
|
331: b"pkey_free",
|
||||||
|
332: b"statx",
|
||||||
|
333: b"io_pgetevents",
|
||||||
|
334: b"rseq",
|
||||||
|
335: b"uretprobe",
|
||||||
|
424: b"pidfd_send_signal",
|
||||||
|
425: b"io_uring_setup",
|
||||||
|
426: b"io_uring_enter",
|
||||||
|
427: b"io_uring_register",
|
||||||
|
428: b"open_tree",
|
||||||
|
429: b"move_mount",
|
||||||
|
430: b"fsopen",
|
||||||
|
431: b"fsconfig",
|
||||||
|
432: b"fsmount",
|
||||||
|
433: b"fspick",
|
||||||
|
434: b"pidfd_open",
|
||||||
|
435: b"clone3",
|
||||||
|
436: b"close_range",
|
||||||
|
437: b"openat2",
|
||||||
|
438: b"pidfd_getfd",
|
||||||
|
439: b"faccessat2",
|
||||||
|
440: b"process_madvise",
|
||||||
|
441: b"epoll_pwait2",
|
||||||
|
442: b"mount_setattr",
|
||||||
|
443: b"quotactl_fd",
|
||||||
|
444: b"landlock_create_ruleset",
|
||||||
|
445: b"landlock_add_rule",
|
||||||
|
446: b"landlock_restrict_self",
|
||||||
|
447: b"memfd_secret",
|
||||||
|
448: b"process_mrelease",
|
||||||
|
449: b"futex_waitv",
|
||||||
|
450: b"set_mempolicy_home_node",
|
||||||
|
451: b"cachestat",
|
||||||
|
452: b"fchmodat2",
|
||||||
|
453: b"map_shadow_stack",
|
||||||
|
454: b"futex_wake",
|
||||||
|
455: b"futex_wait",
|
||||||
|
456: b"futex_requeue",
|
||||||
|
457: b"statmount",
|
||||||
|
458: b"listmount",
|
||||||
|
459: b"lsm_get_self_attr",
|
||||||
|
460: b"lsm_set_self_attr",
|
||||||
|
461: b"lsm_list_modules",
|
||||||
|
462: b"mseal",
|
||||||
|
463: b"setxattrat",
|
||||||
|
464: b"getxattrat",
|
||||||
|
465: b"listxattrat",
|
||||||
|
466: b"removexattrat",
|
||||||
|
467: b"open_tree_attr",
|
||||||
|
468: b"file_getattr",
|
||||||
|
469: b"file_setattr",
|
||||||
|
512: b"rt_sigaction",
|
||||||
|
513: b"rt_sigreturn",
|
||||||
|
514: b"ioctl",
|
||||||
|
515: b"readv",
|
||||||
|
516: b"writev",
|
||||||
|
517: b"recvfrom",
|
||||||
|
518: b"sendmsg",
|
||||||
|
519: b"recvmsg",
|
||||||
|
520: b"execve",
|
||||||
|
521: b"ptrace",
|
||||||
|
522: b"rt_sigpending",
|
||||||
|
523: b"rt_sigtimedwait",
|
||||||
|
524: b"rt_sigqueueinfo",
|
||||||
|
525: b"sigaltstack",
|
||||||
|
526: b"timer_create",
|
||||||
|
527: b"mq_notify",
|
||||||
|
528: b"kexec_load",
|
||||||
|
529: b"waitid",
|
||||||
|
530: b"set_robust_list",
|
||||||
|
531: b"get_robust_list",
|
||||||
|
532: b"vmsplice",
|
||||||
|
533: b"move_pages",
|
||||||
|
534: b"preadv",
|
||||||
|
535: b"pwritev",
|
||||||
|
536: b"rt_tgsigqueueinfo",
|
||||||
|
537: b"recvmmsg",
|
||||||
|
538: b"sendmmsg",
|
||||||
|
539: b"process_vm_readv",
|
||||||
|
540: b"process_vm_writev",
|
||||||
|
541: b"setsockopt",
|
||||||
|
542: b"getsockopt",
|
||||||
|
543: b"io_setup",
|
||||||
|
544: b"io_submit",
|
||||||
|
545: b"execveat",
|
||||||
|
546: b"preadv2",
|
||||||
|
547: b"pwritev2",
|
||||||
|
}
|
||||||
117
examples/anomaly-detection/lib/probe.py
Normal file
117
examples/anomaly-detection/lib/probe.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
"""
|
||||||
|
PythonBPF eBPF Probe for Syscall Histogram Collection
|
||||||
|
"""
|
||||||
|
|
||||||
|
from vmlinux import struct_trace_event_raw_sys_enter
|
||||||
|
from pythonbpf import bpf, map, section, bpfglobal, BPF
|
||||||
|
from pythonbpf.helper import pid
|
||||||
|
from pythonbpf.maps import HashMap
|
||||||
|
from ctypes import c_int64
|
||||||
|
from lib import MAX_SYSCALLS, comm_for_pid
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def histogram() -> HashMap:
|
||||||
|
return HashMap(key=c_int64, value=c_int64, max_entries=1024)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def target_pid_map() -> HashMap:
|
||||||
|
return HashMap(key=c_int64, value=c_int64, max_entries=1)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/raw_syscalls/sys_enter")
|
||||||
|
def trace_syscall(ctx: struct_trace_event_raw_sys_enter) -> c_int64:
|
||||||
|
syscall_id = ctx.id
|
||||||
|
current_pid = pid()
|
||||||
|
target = target_pid_map.lookup(0)
|
||||||
|
if target:
|
||||||
|
if current_pid != target:
|
||||||
|
return 0 # type: ignore
|
||||||
|
if syscall_id < 0 or syscall_id >= 548:
|
||||||
|
return 0 # type: ignore
|
||||||
|
count = histogram.lookup(syscall_id)
|
||||||
|
if count:
|
||||||
|
histogram.update(syscall_id, count + 1)
|
||||||
|
else:
|
||||||
|
histogram.update(syscall_id, 1)
|
||||||
|
return 0 # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
|
||||||
|
ebpf_prog = BPF()
|
||||||
|
|
||||||
|
|
||||||
|
class Probe:
|
||||||
|
"""
|
||||||
|
Syscall histogram probe for a target process.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
probe = Probe(target_pid=1234)
|
||||||
|
probe.start()
|
||||||
|
histogram = probe.get_histogram()
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, target_pid: int, max_syscalls: int = MAX_SYSCALLS):
|
||||||
|
self.target_pid = target_pid
|
||||||
|
self.max_syscalls = max_syscalls
|
||||||
|
self.comm = comm_for_pid(target_pid)
|
||||||
|
|
||||||
|
if self.comm is None:
|
||||||
|
raise ValueError(f"Cannot find process with PID {target_pid}")
|
||||||
|
|
||||||
|
self._bpf = None
|
||||||
|
self._histogram_map = None
|
||||||
|
self._target_map = None
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Compile, load, and attach the BPF probe."""
|
||||||
|
# Compile and load
|
||||||
|
self._bpf = ebpf_prog
|
||||||
|
self._bpf.load()
|
||||||
|
self._bpf.attach_all()
|
||||||
|
|
||||||
|
# Get map references
|
||||||
|
self._histogram_map = self._bpf["histogram"]
|
||||||
|
self._target_map = self._bpf["target_pid_map"]
|
||||||
|
|
||||||
|
# Set target PID in the map
|
||||||
|
self._target_map.update(0, self.target_pid)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def get_histogram(self) -> list:
|
||||||
|
"""Read current histogram values as a list."""
|
||||||
|
if self._histogram_map is None:
|
||||||
|
raise RuntimeError("Probe not started. Call start() first.")
|
||||||
|
|
||||||
|
result = [0] * self.max_syscalls
|
||||||
|
|
||||||
|
for syscall_id in range(self.max_syscalls):
|
||||||
|
try:
|
||||||
|
count = self._histogram_map.lookup(syscall_id)
|
||||||
|
if count is not None:
|
||||||
|
result[syscall_id] = int(count)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def __getitem__(self, syscall_id: int) -> int:
|
||||||
|
"""Allow indexing: probe[syscall_id]"""
|
||||||
|
if self._histogram_map is None:
|
||||||
|
raise RuntimeError("Probe not started")
|
||||||
|
|
||||||
|
try:
|
||||||
|
count = self._histogram_map.lookup(syscall_id)
|
||||||
|
return int(count) if count is not None else 0
|
||||||
|
except Exception:
|
||||||
|
return 0
|
||||||
335
examples/anomaly-detection/main.py
Normal file
335
examples/anomaly-detection/main.py
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Process Behavior Anomaly Detection using PythonBPF and Autoencoders
|
||||||
|
|
||||||
|
Ported from evilsocket's BCC implementation to PythonBPF.
|
||||||
|
https://github.com/evilsocket/ebpf-process-anomaly-detection
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
# 1.Learn normal behavior from a process
|
||||||
|
sudo python main.py --learn --pid 1234 --data normal.csv
|
||||||
|
|
||||||
|
# 2.Train the autoencoder (no sudo needed)
|
||||||
|
python main.py --train --data normal.csv --model model.h5
|
||||||
|
|
||||||
|
# 3.Monitor for anomalies
|
||||||
|
sudo python main.py --run --pid 1234 --model model.h5
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
from lib import MAX_SYSCALLS
|
||||||
|
from lib.ml import AutoEncoder
|
||||||
|
from lib.platform import SYSCALLS
|
||||||
|
from lib.probe import Probe
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def learn(pid: int, data_path: str, poll_interval_ms: int) -> None:
|
||||||
|
"""
|
||||||
|
Capture syscall patterns from target process.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pid: Target process ID
|
||||||
|
data_path: Path to save CSV data
|
||||||
|
poll_interval_ms: Polling interval in milliseconds
|
||||||
|
"""
|
||||||
|
if os.path.exists(data_path):
|
||||||
|
logger.error(
|
||||||
|
f"{data_path} already exists.Delete it or use a different filename."
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
probe = Probe(pid)
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(str(e))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
probe_comm = probe.comm.decode() if probe.comm else "unknown"
|
||||||
|
|
||||||
|
print(f"📊 Learning from process {pid} ({probe_comm})")
|
||||||
|
print(f"📁 Saving data to {data_path}")
|
||||||
|
print(f"⏱️ Polling interval: {poll_interval_ms}ms")
|
||||||
|
print("Press Ctrl+C to stop...\n")
|
||||||
|
|
||||||
|
probe.start()
|
||||||
|
|
||||||
|
prev_histogram = [0.0] * MAX_SYSCALLS
|
||||||
|
prev_report_time = time.time()
|
||||||
|
sample_count = 0
|
||||||
|
poll_interval_sec = poll_interval_ms / 1000.0
|
||||||
|
|
||||||
|
header = "sample_time," + ",".join(f"sys_{i}" for i in range(MAX_SYSCALLS))
|
||||||
|
|
||||||
|
with open(data_path, "w") as fp:
|
||||||
|
fp.write(header + "\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
histogram = [float(x) for x in probe.get_histogram()]
|
||||||
|
|
||||||
|
if histogram != prev_histogram:
|
||||||
|
deltas = _compute_deltas(prev_histogram, histogram)
|
||||||
|
prev_histogram = histogram.copy()
|
||||||
|
|
||||||
|
row = f"{time.time()},{','.join(map(str, deltas))}"
|
||||||
|
fp.write(row + "\n")
|
||||||
|
fp.flush()
|
||||||
|
sample_count += 1
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
if now - prev_report_time >= 1.0:
|
||||||
|
print(f" {sample_count} samples saved...")
|
||||||
|
prev_report_time = now
|
||||||
|
|
||||||
|
time.sleep(poll_interval_sec)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print(f"\n✅ Stopped. Saved {sample_count} samples to {data_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def train(data_path: str, model_path: str, epochs: int, batch_size: int) -> None:
|
||||||
|
"""
|
||||||
|
Train autoencoder on captured data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data_path: Path to training CSV data
|
||||||
|
model_path: Path to save trained model
|
||||||
|
epochs: Number of training epochs
|
||||||
|
batch_size: Training batch size
|
||||||
|
"""
|
||||||
|
if not os.path.exists(data_path):
|
||||||
|
logger.error(f"Data file {data_path} not found.Run --learn first.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"🧠 Training autoencoder on {data_path}")
|
||||||
|
print(f" Epochs: {epochs}")
|
||||||
|
print(f" Batch size: {batch_size}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
ae = AutoEncoder(model_path)
|
||||||
|
_, threshold = ae.train(data_path, epochs, batch_size)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 50)
|
||||||
|
print("✅ Training complete!")
|
||||||
|
print(f" Model saved to: {model_path}")
|
||||||
|
print(f" Error threshold: {threshold:.6f}")
|
||||||
|
print()
|
||||||
|
print(f"💡 Use --max-error {threshold:.4f} when running detection")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
|
||||||
|
def run(pid: int, model_path: str, max_error: float, poll_interval_ms: int) -> None:
|
||||||
|
"""
|
||||||
|
Monitor process and detect anomalies.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pid: Target process ID
|
||||||
|
model_path: Path to trained model
|
||||||
|
max_error: Anomaly detection threshold
|
||||||
|
poll_interval_ms: Polling interval in milliseconds
|
||||||
|
"""
|
||||||
|
if not os.path.exists(model_path):
|
||||||
|
logger.error(f"Model file {model_path} not found. Run --train first.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
probe = Probe(pid)
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(str(e))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
ae = AutoEncoder(model_path, load=True)
|
||||||
|
probe_comm = probe.comm.decode() if probe.comm else "unknown"
|
||||||
|
|
||||||
|
print(f"🔍 Monitoring process {pid} ({probe_comm}) for anomalies")
|
||||||
|
print(f" Error threshold: {max_error}")
|
||||||
|
print(f" Polling interval: {poll_interval_ms}ms")
|
||||||
|
print("Press Ctrl+C to stop...\n")
|
||||||
|
|
||||||
|
probe.start()
|
||||||
|
|
||||||
|
prev_histogram = [0.0] * MAX_SYSCALLS
|
||||||
|
anomaly_count = 0
|
||||||
|
check_count = 0
|
||||||
|
poll_interval_sec = poll_interval_ms / 1000.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
histogram = [float(x) for x in probe.get_histogram()]
|
||||||
|
|
||||||
|
if histogram != prev_histogram:
|
||||||
|
deltas = _compute_deltas(prev_histogram, histogram)
|
||||||
|
prev_histogram = histogram.copy()
|
||||||
|
check_count += 1
|
||||||
|
|
||||||
|
_, feat_errors, total_error = ae.predict([deltas])
|
||||||
|
|
||||||
|
if total_error > max_error:
|
||||||
|
anomaly_count += 1
|
||||||
|
_report_anomaly(anomaly_count, total_error, max_error, feat_errors)
|
||||||
|
|
||||||
|
time.sleep(poll_interval_sec)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n✅ Stopped.")
|
||||||
|
print(f" Checks performed: {check_count}")
|
||||||
|
print(f" Anomalies detected: {anomaly_count}")
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_deltas(prev: list[float], current: list[float]) -> list[float]:
|
||||||
|
"""Compute rate of change between two histograms."""
|
||||||
|
deltas = []
|
||||||
|
for p, c in zip(prev, current):
|
||||||
|
if c != 0.0:
|
||||||
|
delta = 1.0 - (p / c)
|
||||||
|
else:
|
||||||
|
delta = 0.0
|
||||||
|
deltas.append(delta)
|
||||||
|
return deltas
|
||||||
|
|
||||||
|
|
||||||
|
def _report_anomaly(
|
||||||
|
count: int,
|
||||||
|
total_error: float,
|
||||||
|
threshold: float,
|
||||||
|
feat_errors: list[float],
|
||||||
|
) -> None:
|
||||||
|
"""Print anomaly report with top offending syscalls."""
|
||||||
|
print(f"🚨 ANOMALY #{count} detected!")
|
||||||
|
print(f" Total error: {total_error:.4f} (threshold: {threshold})")
|
||||||
|
|
||||||
|
errors_by_syscall = {idx: err for idx, err in enumerate(feat_errors)}
|
||||||
|
top3 = Counter(errors_by_syscall).most_common(3)
|
||||||
|
|
||||||
|
print(" Top anomalous syscalls:")
|
||||||
|
for idx, err in top3:
|
||||||
|
name = SYSCALLS.get(idx, f"syscall_{idx}")
|
||||||
|
print(f" • {name!r}: {err:.4f}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
"""Parse command line arguments."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Process anomaly detection with PythonBPF and Autoencoders",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
Examples:
|
||||||
|
# Learn from a process (e.g., Firefox) for a few minutes
|
||||||
|
sudo python main.py --learn --pid $(pgrep -o firefox) --data firefox.csv
|
||||||
|
|
||||||
|
# Train the model (no sudo needed)
|
||||||
|
python main.py --train --data firefox.csv --model firefox.h5
|
||||||
|
|
||||||
|
# Monitor the same process for anomalies
|
||||||
|
sudo python main.py --run --pid $(pgrep -o firefox) --model firefox.h5
|
||||||
|
|
||||||
|
# Full workflow for nginx:
|
||||||
|
sudo python main.py --learn --pid $(pgrep -o nginx) --data nginx_normal.csv
|
||||||
|
python main.py --train --data nginx_normal.csv --model nginx.h5 --epochs 100
|
||||||
|
sudo python main.py --run --pid $(pgrep -o nginx) --model nginx.h5 --max-error 0.05
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
actions = parser.add_mutually_exclusive_group()
|
||||||
|
actions.add_argument(
|
||||||
|
"--learn",
|
||||||
|
action="store_true",
|
||||||
|
help="Capture syscall patterns from a process",
|
||||||
|
)
|
||||||
|
actions.add_argument(
|
||||||
|
"--train",
|
||||||
|
action="store_true",
|
||||||
|
help="Train autoencoder on captured data",
|
||||||
|
)
|
||||||
|
actions.add_argument(
|
||||||
|
"--run",
|
||||||
|
action="store_true",
|
||||||
|
help="Monitor process for anomalies",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--pid",
|
||||||
|
type=int,
|
||||||
|
default=0,
|
||||||
|
help="Target process ID",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--data",
|
||||||
|
default="data.csv",
|
||||||
|
help="CSV file for training data (default: data.csv)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--model",
|
||||||
|
default="model.keras",
|
||||||
|
help="Model file path (default: model.h5)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--time",
|
||||||
|
type=int,
|
||||||
|
default=100,
|
||||||
|
help="Polling interval in milliseconds (default: 100)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--epochs",
|
||||||
|
type=int,
|
||||||
|
default=200,
|
||||||
|
help="Training epochs (default: 200)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--batch-size",
|
||||||
|
type=int,
|
||||||
|
default=16,
|
||||||
|
help="Training batch size (default: 16)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--max-error",
|
||||||
|
type=float,
|
||||||
|
default=0.09,
|
||||||
|
help="Anomaly detection threshold (default: 0.09)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Main entry point."""
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
if not any([args.learn, args.train, args.run]):
|
||||||
|
print("No action specified.Use --learn, --train, or --run.")
|
||||||
|
print("Run with --help for usage information.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if args.learn:
|
||||||
|
if args.pid == 0:
|
||||||
|
logger.error("--pid required for --learn")
|
||||||
|
sys.exit(1)
|
||||||
|
learn(args.pid, args.data, args.time)
|
||||||
|
|
||||||
|
elif args.train:
|
||||||
|
train(args.data, args.model, args.epochs, args.batch_size)
|
||||||
|
|
||||||
|
elif args.run:
|
||||||
|
if args.pid == 0:
|
||||||
|
logger.error("--pid required for --run")
|
||||||
|
sys.exit(1)
|
||||||
|
run(args.pid, args.model, args.max_error, args.time)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -1,15 +1,16 @@
|
|||||||
from pythonbpf import bpf, map, section, bpfglobal, compile
|
from pythonbpf import bpf, map, section, bpfglobal, compile
|
||||||
from pythonbpf.helpers import ktime
|
from pythonbpf.helper import ktime
|
||||||
from pythonbpf.maps import HashMap
|
from pythonbpf.maps import HashMap
|
||||||
|
|
||||||
from ctypes import c_void_p, c_int64, c_uint64
|
from ctypes import c_void_p, c_int64, c_uint64
|
||||||
|
|
||||||
# Instructions to how to run this program
|
# Instructions to how to run this program
|
||||||
# 1. Install PythonBPF: pip install pythonbpf
|
# 1. Install PythonBPF: pip install pythonbpf
|
||||||
# 2. Run the program: python demo/pybpf3.py
|
# 2. Run the program: python examples/binops_demo.py
|
||||||
# 3. Run the program with sudo: sudo examples/check.sh run demo/pybpf3.o
|
# 3. Run the program with sudo: sudo tools/check.sh run examples/binops_demo.py
|
||||||
# 4. Start up any program and watch the output
|
# 4. Start up any program and watch the output
|
||||||
|
|
||||||
|
|
||||||
@bpf
|
@bpf
|
||||||
@map
|
@map
|
||||||
def last() -> HashMap:
|
def last() -> HashMap:
|
||||||
@ -20,29 +21,31 @@ def last() -> HashMap:
|
|||||||
@section("tracepoint/syscalls/sys_enter_execve")
|
@section("tracepoint/syscalls/sys_enter_execve")
|
||||||
def do_trace(ctx: c_void_p) -> c_int64:
|
def do_trace(ctx: c_void_p) -> c_int64:
|
||||||
key = 0
|
key = 0
|
||||||
tsp = last().lookup(key)
|
tsp = last.lookup(key)
|
||||||
if tsp:
|
if tsp:
|
||||||
kt = ktime()
|
kt = ktime()
|
||||||
delta = (kt - tsp)
|
delta = kt - tsp
|
||||||
if delta < 1000000000:
|
if delta < 1000000000:
|
||||||
time_ms = (delta // 1000000)
|
time_ms = delta // 1000000
|
||||||
print(f"Execve syscall entered within last second, last {time_ms} ms ago")
|
print(f"Execve syscall entered within last second, last {time_ms} ms ago")
|
||||||
last().delete(key)
|
last.delete(key)
|
||||||
else:
|
else:
|
||||||
kt = ktime()
|
kt = ktime()
|
||||||
last().update(key, kt)
|
last.update(key, kt)
|
||||||
return c_int64(0)
|
return c_int64(0)
|
||||||
|
|
||||||
|
|
||||||
@bpf
|
@bpf
|
||||||
@section("tracepoint/syscalls/sys_exit_execve")
|
@section("tracepoint/syscalls/sys_exit_execve")
|
||||||
def do_exit(ctx: c_void_p) -> c_int64:
|
def do_exit(ctx: c_void_p) -> c_int64:
|
||||||
va = 8
|
va = 8
|
||||||
nm = 5 ^ va
|
nm = 5 ^ va
|
||||||
al = 6 & 3
|
al = 6 & 3
|
||||||
ru = (nm + al)
|
ru = nm + al
|
||||||
print(f"this is a variable {ru}")
|
print(f"this is a variable {ru}")
|
||||||
return c_int64(0)
|
return c_int64(0)
|
||||||
|
|
||||||
|
|
||||||
@bpf
|
@bpf
|
||||||
@bpfglobal
|
@bpfglobal
|
||||||
def LICENSE() -> str:
|
def LICENSE() -> str:
|
||||||
@ -1,8 +1,8 @@
|
|||||||
from pythonbpf import bpf, map, section, bpfglobal, compile
|
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 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
|
@bpf
|
||||||
@ -15,7 +15,7 @@ def last() -> HashMap:
|
|||||||
@section("blk_start_request")
|
@section("blk_start_request")
|
||||||
def trace_start(ctx: c_void_p) -> c_int32:
|
def trace_start(ctx: c_void_p) -> c_int32:
|
||||||
ts = ktime()
|
ts = ktime()
|
||||||
print("req started")
|
print(f"req started {ts}")
|
||||||
return c_int32(0)
|
return c_int32(0)
|
||||||
|
|
||||||
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
BPF_CLANG := clang
|
|
||||||
CFLAGS := -O2 -emit-llvm -target bpf -c
|
|
||||||
|
|
||||||
SRC := $(wildcard *.bpf.c)
|
|
||||||
LL := $(SRC:.bpf.c=.bpf.ll)
|
|
||||||
OBJ := $(SRC:.bpf.c=.bpf.o)
|
|
||||||
|
|
||||||
.PHONY: all clean
|
|
||||||
|
|
||||||
all: $(LL) $(OBJ)
|
|
||||||
|
|
||||||
%.bpf.o: %.bpf.c
|
|
||||||
$(BPF_CLANG) -O2 -g -target bpf -c $< -o $@
|
|
||||||
|
|
||||||
%.bpf.ll: %.bpf.c
|
|
||||||
$(BPF_CLANG) $(CFLAGS) -g -S $< -o $@
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -f $(LL) $(OBJ)
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
#define __TARGET_ARCH_arm64
|
|
||||||
|
|
||||||
#include "vmlinux.h"
|
|
||||||
#include <bpf/bpf_helpers.h>
|
|
||||||
#include <bpf/bpf_tracing.h>
|
|
||||||
#include <bpf/bpf_core_read.h>
|
|
||||||
|
|
||||||
// Map: key = struct request*, value = u64 timestamp
|
|
||||||
struct {
|
|
||||||
__uint(type, BPF_MAP_TYPE_HASH);
|
|
||||||
__type(key, struct request *);
|
|
||||||
__type(value, u64);
|
|
||||||
__uint(max_entries, 1024);
|
|
||||||
} start SEC(".maps");
|
|
||||||
|
|
||||||
// Attach to kprobe for blk_start_request
|
|
||||||
SEC("kprobe/blk_start_request")
|
|
||||||
int BPF_KPROBE(trace_start, struct request *req)
|
|
||||||
{
|
|
||||||
u64 ts = bpf_ktime_get_ns();
|
|
||||||
bpf_map_update_elem(&start, &req, &ts, BPF_ANY);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
char LICENSE[] SEC("license") = "GPL";
|
|
||||||
121617
examples/c-form/vmlinux.h
121617
examples/c-form/vmlinux.h
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -1,9 +1,8 @@
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from pythonbpf import bpf, map, section, bpfglobal, BPF
|
from pythonbpf import bpf, map, section, bpfglobal, BPF
|
||||||
from pythonbpf.helpers import pid
|
from pythonbpf.helper import pid
|
||||||
from pythonbpf.maps import HashMap
|
from pythonbpf.maps import HashMap
|
||||||
from pylibbpf import *
|
|
||||||
from ctypes import c_void_p, c_int64, c_uint64, c_int32
|
from ctypes import c_void_p, c_int64, c_uint64, c_int32
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
@ -12,26 +11,28 @@ import matplotlib.pyplot as plt
|
|||||||
# and then plots the distribution as a histogram using matplotlib.
|
# and then plots the distribution as a histogram using matplotlib.
|
||||||
# It provides a quick view of process creation activity over 10 seconds.
|
# 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.
|
# 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
|
@bpf
|
||||||
@map
|
@map
|
||||||
def hist() -> HashMap:
|
def hist() -> HashMap:
|
||||||
return HashMap(key=c_int32, value=c_uint64, max_entries=4096)
|
return HashMap(key=c_int32, value=c_uint64, max_entries=4096)
|
||||||
|
|
||||||
|
|
||||||
@bpf
|
@bpf
|
||||||
@section("tracepoint/syscalls/sys_enter_clone")
|
@section("tracepoint/syscalls/sys_enter_clone")
|
||||||
def hello(ctx: c_void_p) -> c_int64:
|
def hello(ctx: c_void_p) -> c_int64:
|
||||||
process_id = pid()
|
process_id = pid()
|
||||||
one = 1
|
one = 1
|
||||||
prev = hist().lookup(process_id)
|
prev = hist.lookup(process_id)
|
||||||
if prev:
|
if prev:
|
||||||
previous_value = prev + 1
|
previous_value = prev + 1
|
||||||
print(f"count: {previous_value} with {process_id}")
|
print(f"count: {previous_value} with {process_id}")
|
||||||
hist().update(process_id, previous_value)
|
hist.update(process_id, previous_value)
|
||||||
return c_int64(0)
|
return c_int64(0)
|
||||||
else:
|
else:
|
||||||
hist().update(process_id, one)
|
hist.update(process_id, one)
|
||||||
return c_int64(0)
|
return c_int64(0)
|
||||||
|
|
||||||
|
|
||||||
@ -42,12 +43,12 @@ def LICENSE() -> str:
|
|||||||
|
|
||||||
|
|
||||||
b = BPF()
|
b = BPF()
|
||||||
b.load_and_attach()
|
b.load()
|
||||||
hist = BpfMap(b, hist)
|
b.attach_all()
|
||||||
print("Recording")
|
print("Recording")
|
||||||
time.sleep(10)
|
time.sleep(10)
|
||||||
|
|
||||||
counts = list(hist.values())
|
counts = list(b["hist"].values())
|
||||||
|
|
||||||
plt.hist(counts, bins=20)
|
plt.hist(counts, bins=20)
|
||||||
plt.xlabel("Clone calls per PID")
|
plt.xlabel("Clone calls per PID")
|
||||||
49
examples/container-monitor/README.md
Normal file
49
examples/container-monitor/README.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# Container Monitor TUI
|
||||||
|
|
||||||
|
A beautiful terminal-based container monitoring tool that combines syscall tracking, file I/O monitoring, and network traffic analysis using eBPF.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🎯 **Interactive Cgroup Selection** - Navigate and select cgroups with arrow keys
|
||||||
|
- 📊 **Real-time Monitoring** - Live graphs and statistics
|
||||||
|
- 🔥 **Syscall Tracking** - Total syscall count per cgroup
|
||||||
|
- 💾 **File I/O Monitoring** - Read/write operations and bytes with graphs
|
||||||
|
- 🌐 **Network Traffic** - RX/TX packets and bytes with live graphs
|
||||||
|
- ⚡ **Efficient Caching** - Reduced /proc lookups for better performance
|
||||||
|
- 🎨 **Beautiful TUI** - Clean, colorful terminal interface
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.7+
|
||||||
|
- pythonbpf
|
||||||
|
- Root privileges (for eBPF)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ensure you have pythonbpf installed
|
||||||
|
pip install pythonbpf
|
||||||
|
|
||||||
|
# Run the monitor
|
||||||
|
sudo $(which python) container_monitor.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. **Selection Screen**: Use ↑↓ arrow keys to navigate through cgroups, press ENTER to select
|
||||||
|
2. **Monitoring Screen**: View real-time graphs and statistics, press ESC or 'b' to go back
|
||||||
|
3. **Exit**: Press 'q' at any time to quit
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- `container_monitor.py` - Main BPF program combining all three tracers
|
||||||
|
- `data_collector.py` - Data collection, caching, and history management
|
||||||
|
- `tui. py` - Terminal user interface with selection and monitoring screens
|
||||||
|
|
||||||
|
## BPF Programs
|
||||||
|
|
||||||
|
- **vfs_read/vfs_write** - Track file I/O operations
|
||||||
|
- **__netif_receive_skb/__dev_queue_xmit** - Track network traffic
|
||||||
|
- **raw_syscalls/sys_enter** - Count all syscalls
|
||||||
|
|
||||||
|
All programs filter by cgroup ID for per-container monitoring.
|
||||||
220
examples/container-monitor/container_monitor.py
Normal file
220
examples/container-monitor/container_monitor.py
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
"""Container Monitor - TUI-based cgroup monitoring combining syscall, file I/O, and network tracking."""
|
||||||
|
|
||||||
|
from pythonbpf import bpf, map, section, bpfglobal, struct, BPF
|
||||||
|
from pythonbpf.maps import HashMap
|
||||||
|
from pythonbpf.helper import get_current_cgroup_id
|
||||||
|
from ctypes import c_int32, c_uint64, c_void_p
|
||||||
|
from vmlinux import struct_pt_regs, struct_sk_buff
|
||||||
|
|
||||||
|
from data_collection import ContainerDataCollector
|
||||||
|
from tui import ContainerMonitorTUI
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== BPF Structs ====================
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class read_stats:
|
||||||
|
bytes: c_uint64
|
||||||
|
ops: c_uint64
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class write_stats:
|
||||||
|
bytes: c_uint64
|
||||||
|
ops: c_uint64
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@struct
|
||||||
|
class net_stats:
|
||||||
|
rx_packets: c_uint64
|
||||||
|
tx_packets: c_uint64
|
||||||
|
rx_bytes: c_uint64
|
||||||
|
tx_bytes: c_uint64
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== BPF Maps ====================
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def read_map() -> HashMap:
|
||||||
|
return HashMap(key=c_uint64, value=read_stats, max_entries=1024)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def write_map() -> HashMap:
|
||||||
|
return HashMap(key=c_uint64, value=write_stats, max_entries=1024)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def net_stats_map() -> HashMap:
|
||||||
|
return HashMap(key=c_uint64, value=net_stats, max_entries=1024)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@map
|
||||||
|
def syscall_count() -> HashMap:
|
||||||
|
return HashMap(key=c_uint64, value=c_uint64, max_entries=1024)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== File I/O Tracing ====================
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("kprobe/vfs_read")
|
||||||
|
def trace_read(ctx: struct_pt_regs) -> c_int32:
|
||||||
|
cg = get_current_cgroup_id()
|
||||||
|
count = c_uint64(ctx.dx)
|
||||||
|
ptr = read_map.lookup(cg)
|
||||||
|
if ptr:
|
||||||
|
s = read_stats()
|
||||||
|
s.bytes = ptr.bytes + count
|
||||||
|
s.ops = ptr.ops + 1
|
||||||
|
read_map.update(cg, s)
|
||||||
|
else:
|
||||||
|
s = read_stats()
|
||||||
|
s.bytes = count
|
||||||
|
s.ops = c_uint64(1)
|
||||||
|
read_map.update(cg, s)
|
||||||
|
|
||||||
|
return c_int32(0)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("kprobe/vfs_write")
|
||||||
|
def trace_write(ctx1: struct_pt_regs) -> c_int32:
|
||||||
|
cg = get_current_cgroup_id()
|
||||||
|
count = c_uint64(ctx1.dx)
|
||||||
|
ptr = write_map.lookup(cg)
|
||||||
|
|
||||||
|
if ptr:
|
||||||
|
s = write_stats()
|
||||||
|
s.bytes = ptr.bytes + count
|
||||||
|
s.ops = ptr.ops + 1
|
||||||
|
write_map.update(cg, s)
|
||||||
|
else:
|
||||||
|
s = write_stats()
|
||||||
|
s.bytes = count
|
||||||
|
s.ops = c_uint64(1)
|
||||||
|
write_map.update(cg, s)
|
||||||
|
|
||||||
|
return c_int32(0)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== Network I/O Tracing ====================
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("kprobe/__netif_receive_skb")
|
||||||
|
def trace_netif_rx(ctx2: struct_pt_regs) -> c_int32:
|
||||||
|
cgroup_id = get_current_cgroup_id()
|
||||||
|
skb = struct_sk_buff(ctx2.di)
|
||||||
|
pkt_len = c_uint64(skb.len)
|
||||||
|
|
||||||
|
stats_ptr = net_stats_map.lookup(cgroup_id)
|
||||||
|
|
||||||
|
if stats_ptr:
|
||||||
|
stats = net_stats()
|
||||||
|
stats.rx_packets = stats_ptr.rx_packets + 1
|
||||||
|
stats.tx_packets = stats_ptr.tx_packets
|
||||||
|
stats.rx_bytes = stats_ptr.rx_bytes + pkt_len
|
||||||
|
stats.tx_bytes = stats_ptr.tx_bytes
|
||||||
|
net_stats_map.update(cgroup_id, stats)
|
||||||
|
else:
|
||||||
|
stats = net_stats()
|
||||||
|
stats.rx_packets = c_uint64(1)
|
||||||
|
stats.tx_packets = c_uint64(0)
|
||||||
|
stats.rx_bytes = pkt_len
|
||||||
|
stats.tx_bytes = c_uint64(0)
|
||||||
|
net_stats_map.update(cgroup_id, stats)
|
||||||
|
|
||||||
|
return c_int32(0)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("kprobe/__dev_queue_xmit")
|
||||||
|
def trace_dev_xmit(ctx3: struct_pt_regs) -> c_int32:
|
||||||
|
cgroup_id = get_current_cgroup_id()
|
||||||
|
skb = struct_sk_buff(ctx3.di)
|
||||||
|
pkt_len = c_uint64(skb.len)
|
||||||
|
|
||||||
|
stats_ptr = net_stats_map.lookup(cgroup_id)
|
||||||
|
|
||||||
|
if stats_ptr:
|
||||||
|
stats = net_stats()
|
||||||
|
stats.rx_packets = stats_ptr.rx_packets
|
||||||
|
stats.tx_packets = stats_ptr.tx_packets + 1
|
||||||
|
stats.rx_bytes = stats_ptr.rx_bytes
|
||||||
|
stats.tx_bytes = stats_ptr.tx_bytes + pkt_len
|
||||||
|
net_stats_map.update(cgroup_id, stats)
|
||||||
|
else:
|
||||||
|
stats = net_stats()
|
||||||
|
stats.rx_packets = c_uint64(0)
|
||||||
|
stats.tx_packets = c_uint64(1)
|
||||||
|
stats.rx_bytes = c_uint64(0)
|
||||||
|
stats.tx_bytes = pkt_len
|
||||||
|
net_stats_map.update(cgroup_id, stats)
|
||||||
|
|
||||||
|
return c_int32(0)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== Syscall Tracing ====================
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@section("tracepoint/raw_syscalls/sys_enter")
|
||||||
|
def count_syscalls(ctx: c_void_p) -> c_int32:
|
||||||
|
cgroup_id = get_current_cgroup_id()
|
||||||
|
count_ptr = syscall_count.lookup(cgroup_id)
|
||||||
|
|
||||||
|
if count_ptr:
|
||||||
|
new_count = count_ptr + c_uint64(1)
|
||||||
|
syscall_count.update(cgroup_id, new_count)
|
||||||
|
else:
|
||||||
|
syscall_count.update(cgroup_id, c_uint64(1))
|
||||||
|
|
||||||
|
return c_int32(0)
|
||||||
|
|
||||||
|
|
||||||
|
@bpf
|
||||||
|
@bpfglobal
|
||||||
|
def LICENSE() -> str:
|
||||||
|
return "GPL"
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== Main ====================
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("🔥 Loading BPF programs...")
|
||||||
|
|
||||||
|
# Load and attach BPF program
|
||||||
|
b = BPF()
|
||||||
|
b.load()
|
||||||
|
b.attach_all()
|
||||||
|
|
||||||
|
# Get map references and enable struct deserialization
|
||||||
|
read_map_ref = b["read_map"]
|
||||||
|
write_map_ref = b["write_map"]
|
||||||
|
net_stats_map_ref = b["net_stats_map"]
|
||||||
|
syscall_count_ref = b["syscall_count"]
|
||||||
|
|
||||||
|
read_map_ref.set_value_struct("read_stats")
|
||||||
|
write_map_ref.set_value_struct("write_stats")
|
||||||
|
net_stats_map_ref.set_value_struct("net_stats")
|
||||||
|
|
||||||
|
print("✅ BPF programs loaded and attached")
|
||||||
|
|
||||||
|
# Setup data collector
|
||||||
|
collector = ContainerDataCollector(
|
||||||
|
read_map_ref, write_map_ref, net_stats_map_ref, syscall_count_ref
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create and run TUI
|
||||||
|
tui = ContainerMonitorTUI(collector)
|
||||||
|
tui.run()
|
||||||
208
examples/container-monitor/data_collection.py
Normal file
208
examples/container-monitor/data_collection.py
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
"""Data collection and management for container monitoring."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Set, Optional
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from collections import deque, defaultdict
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CgroupInfo:
|
||||||
|
"""Information about a cgroup."""
|
||||||
|
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
path: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ContainerStats:
|
||||||
|
"""Statistics for a container/cgroup."""
|
||||||
|
|
||||||
|
cgroup_id: int
|
||||||
|
cgroup_name: str
|
||||||
|
|
||||||
|
# File I/O
|
||||||
|
read_ops: int = 0
|
||||||
|
read_bytes: int = 0
|
||||||
|
write_ops: int = 0
|
||||||
|
write_bytes: int = 0
|
||||||
|
|
||||||
|
# Network I/O
|
||||||
|
rx_packets: int = 0
|
||||||
|
rx_bytes: int = 0
|
||||||
|
tx_packets: int = 0
|
||||||
|
tx_bytes: int = 0
|
||||||
|
|
||||||
|
# Syscalls
|
||||||
|
syscall_count: int = 0
|
||||||
|
|
||||||
|
# Timestamp
|
||||||
|
timestamp: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
class ContainerDataCollector:
|
||||||
|
"""Collects and manages container monitoring data from BPF."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, read_map, write_map, net_stats_map, syscall_map, history_size: int = 100
|
||||||
|
):
|
||||||
|
self.read_map = read_map
|
||||||
|
self.write_map = write_map
|
||||||
|
self.net_stats_map = net_stats_map
|
||||||
|
self.syscall_map = syscall_map
|
||||||
|
|
||||||
|
# Caching
|
||||||
|
self._cgroup_cache: Dict[int, CgroupInfo] = {}
|
||||||
|
self._cgroup_cache_time = 0
|
||||||
|
self._cache_ttl = 5.0
|
||||||
|
0 # Refresh cache every 5 seconds
|
||||||
|
|
||||||
|
# Historical data for graphing
|
||||||
|
self._history_size = history_size
|
||||||
|
self._history: Dict[int, deque] = defaultdict(
|
||||||
|
lambda: deque(maxlen=history_size)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_all_cgroups(self) -> List[CgroupInfo]:
|
||||||
|
"""Get all cgroups with caching."""
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
# Use cached data if still valid
|
||||||
|
if current_time - self._cgroup_cache_time < self._cache_ttl:
|
||||||
|
return list(self._cgroup_cache.values())
|
||||||
|
|
||||||
|
# Refresh cache
|
||||||
|
self._refresh_cgroup_cache()
|
||||||
|
return list(self._cgroup_cache.values())
|
||||||
|
|
||||||
|
def _refresh_cgroup_cache(self):
|
||||||
|
"""Refresh the cgroup cache from /proc."""
|
||||||
|
cgroup_map: Dict[int, Set[str]] = defaultdict(set)
|
||||||
|
|
||||||
|
# Scan /proc to find all cgroups
|
||||||
|
for proc_dir in Path("/proc").glob("[0-9]*"):
|
||||||
|
try:
|
||||||
|
cgroup_file = proc_dir / "cgroup"
|
||||||
|
if not cgroup_file.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
with open(cgroup_file) as f:
|
||||||
|
for line in f:
|
||||||
|
parts = line.strip().split(":")
|
||||||
|
if len(parts) >= 3:
|
||||||
|
cgroup_path = parts[2]
|
||||||
|
cgroup_mount = f"/sys/fs/cgroup{cgroup_path}"
|
||||||
|
|
||||||
|
if os.path.exists(cgroup_mount):
|
||||||
|
stat_info = os.stat(cgroup_mount)
|
||||||
|
cgroup_id = stat_info.st_ino
|
||||||
|
cgroup_map[cgroup_id].add(cgroup_path)
|
||||||
|
|
||||||
|
except (PermissionError, FileNotFoundError, OSError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Update cache with best names
|
||||||
|
new_cache = {}
|
||||||
|
for cgroup_id, paths in cgroup_map.items():
|
||||||
|
# Pick the most descriptive path
|
||||||
|
best_path = self._get_best_cgroup_path(paths)
|
||||||
|
name = self._get_cgroup_name(best_path)
|
||||||
|
|
||||||
|
new_cache[cgroup_id] = CgroupInfo(id=cgroup_id, name=name, path=best_path)
|
||||||
|
|
||||||
|
self._cgroup_cache = new_cache
|
||||||
|
self._cgroup_cache_time = time.time()
|
||||||
|
|
||||||
|
def _get_best_cgroup_path(self, paths: Set[str]) -> str:
|
||||||
|
"""Select the most descriptive cgroup path."""
|
||||||
|
path_list = list(paths)
|
||||||
|
|
||||||
|
# Prefer paths with more components (more specific)
|
||||||
|
# Prefer paths containing docker, podman, etc.
|
||||||
|
for keyword in ["docker", "podman", "kubernetes", "k8s", "systemd"]:
|
||||||
|
for path in path_list:
|
||||||
|
if keyword in path.lower():
|
||||||
|
return path
|
||||||
|
|
||||||
|
# Return longest path (most specific)
|
||||||
|
return max(path_list, key=lambda p: (len(p.split("/")), len(p)))
|
||||||
|
|
||||||
|
def _get_cgroup_name(self, path: str) -> str:
|
||||||
|
"""Extract a friendly name from cgroup path."""
|
||||||
|
if not path or path == "/":
|
||||||
|
return "root"
|
||||||
|
|
||||||
|
# Remove leading/trailing slashes
|
||||||
|
path = path.strip("/")
|
||||||
|
|
||||||
|
# Try to extract container ID or service name
|
||||||
|
parts = path.split("/")
|
||||||
|
|
||||||
|
# For Docker: /docker/<container_id>
|
||||||
|
if "docker" in path.lower():
|
||||||
|
for i, part in enumerate(parts):
|
||||||
|
if part.lower() == "docker" and i + 1 < len(parts):
|
||||||
|
container_id = parts[i + 1][:12] # Short ID
|
||||||
|
return f"docker:{container_id}"
|
||||||
|
|
||||||
|
# For systemd services
|
||||||
|
if "system.slice" in path:
|
||||||
|
for part in parts:
|
||||||
|
if part.endswith(".service"):
|
||||||
|
return part.replace(".service", "")
|
||||||
|
|
||||||
|
# For user slices
|
||||||
|
if "user.slice" in path:
|
||||||
|
return f"user:{parts[-1]}" if parts else "user"
|
||||||
|
|
||||||
|
# Default: use last component
|
||||||
|
return parts[-1] if parts else path
|
||||||
|
|
||||||
|
def get_stats_for_cgroup(self, cgroup_id: int) -> ContainerStats:
|
||||||
|
"""Get current statistics for a specific cgroup."""
|
||||||
|
cgroup_info = self._cgroup_cache.get(cgroup_id)
|
||||||
|
cgroup_name = cgroup_info.name if cgroup_info else f"cgroup-{cgroup_id}"
|
||||||
|
|
||||||
|
stats = ContainerStats(
|
||||||
|
cgroup_id=cgroup_id, cgroup_name=cgroup_name, timestamp=time.time()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get file I/O stats
|
||||||
|
read_stat = self.read_map.lookup(cgroup_id)
|
||||||
|
if read_stat:
|
||||||
|
stats.read_ops = int(read_stat.ops)
|
||||||
|
stats.read_bytes = int(read_stat.bytes)
|
||||||
|
|
||||||
|
write_stat = self.write_map.lookup(cgroup_id)
|
||||||
|
if write_stat:
|
||||||
|
stats.write_ops = int(write_stat.ops)
|
||||||
|
stats.write_bytes = int(write_stat.bytes)
|
||||||
|
|
||||||
|
# Get network stats
|
||||||
|
net_stat = self.net_stats_map.lookup(cgroup_id)
|
||||||
|
if net_stat:
|
||||||
|
stats.rx_packets = int(net_stat.rx_packets)
|
||||||
|
stats.rx_bytes = int(net_stat.rx_bytes)
|
||||||
|
stats.tx_packets = int(net_stat.tx_packets)
|
||||||
|
stats.tx_bytes = int(net_stat.tx_bytes)
|
||||||
|
|
||||||
|
# Get syscall count
|
||||||
|
syscall_cnt = self.syscall_map.lookup(cgroup_id)
|
||||||
|
if syscall_cnt is not None:
|
||||||
|
stats.syscall_count = int(syscall_cnt)
|
||||||
|
|
||||||
|
# Add to history
|
||||||
|
self._history[cgroup_id].append(stats)
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def get_history(self, cgroup_id: int) -> List[ContainerStats]:
|
||||||
|
"""Get historical statistics for graphing."""
|
||||||
|
return list(self._history[cgroup_id])
|
||||||
|
|
||||||
|
def get_cgroup_info(self, cgroup_id: int) -> Optional[CgroupInfo]:
|
||||||
|
"""Get cached cgroup information."""
|
||||||
|
return self._cgroup_cache.get(cgroup_id)
|
||||||
752
examples/container-monitor/tui.py
Normal file
752
examples/container-monitor/tui.py
Normal file
@ -0,0 +1,752 @@
|
|||||||
|
"""Terminal User Interface for container monitoring."""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import curses
|
||||||
|
import threading
|
||||||
|
from typing import Optional, List
|
||||||
|
from data_collection import ContainerDataCollector
|
||||||
|
from web_dashboard import WebDashboard
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_addstr(stdscr, y: int, x: int, text: str, *args):
|
||||||
|
"""Safely add string to screen with bounds checking."""
|
||||||
|
try:
|
||||||
|
height, width = stdscr.getmaxyx()
|
||||||
|
if 0 <= y < height and 0 <= x < width:
|
||||||
|
# Truncate text to fit
|
||||||
|
max_len = width - x - 1
|
||||||
|
if max_len > 0:
|
||||||
|
stdscr.addstr(y, x, text[:max_len], *args)
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_fancy_header(stdscr, title: str, subtitle: str):
|
||||||
|
"""Draw a fancy header with title and subtitle."""
|
||||||
|
height, width = stdscr.getmaxyx()
|
||||||
|
|
||||||
|
# Top border
|
||||||
|
_safe_addstr(stdscr, 0, 0, "═" * width, curses.color_pair(6) | curses.A_BOLD)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr,
|
||||||
|
0,
|
||||||
|
max(0, (width - len(title)) // 2),
|
||||||
|
f" {title} ",
|
||||||
|
curses.color_pair(6) | curses.A_BOLD,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Subtitle
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr,
|
||||||
|
1,
|
||||||
|
max(0, (width - len(subtitle)) // 2),
|
||||||
|
subtitle,
|
||||||
|
curses.color_pair(1),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bottom border
|
||||||
|
_safe_addstr(stdscr, 2, 0, "═" * width, curses.color_pair(6))
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_metric_box(
|
||||||
|
stdscr,
|
||||||
|
y: int,
|
||||||
|
x: int,
|
||||||
|
width: int,
|
||||||
|
label: str,
|
||||||
|
value: str,
|
||||||
|
detail: str,
|
||||||
|
color_pair: int,
|
||||||
|
):
|
||||||
|
"""Draw a fancy box for displaying a metric."""
|
||||||
|
height, _ = stdscr.getmaxyx()
|
||||||
|
|
||||||
|
if y + 4 >= height:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Top border
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr, y, x, "┌" + "─" * (width - 2) + "┐", color_pair | curses.A_BOLD
|
||||||
|
)
|
||||||
|
|
||||||
|
# Label
|
||||||
|
_safe_addstr(stdscr, y + 1, x, "│", color_pair | curses.A_BOLD)
|
||||||
|
_safe_addstr(stdscr, y + 1, x + 2, label, color_pair | curses.A_BOLD)
|
||||||
|
_safe_addstr(stdscr, y + 1, x + width - 1, "│", color_pair | curses.A_BOLD)
|
||||||
|
|
||||||
|
# Value
|
||||||
|
_safe_addstr(stdscr, y + 2, x, "│", color_pair | curses.A_BOLD)
|
||||||
|
_safe_addstr(stdscr, y + 2, x + 4, value, curses.color_pair(2) | curses.A_BOLD)
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr,
|
||||||
|
y + 2,
|
||||||
|
min(x + width - len(detail) - 3, x + width - 2),
|
||||||
|
detail,
|
||||||
|
color_pair | curses.A_BOLD,
|
||||||
|
)
|
||||||
|
_safe_addstr(stdscr, y + 2, x + width - 1, "│", color_pair | curses.A_BOLD)
|
||||||
|
|
||||||
|
# Bottom border
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr, y + 3, x, "└" + "─" * (width - 2) + "┘", color_pair | curses.A_BOLD
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_section_header(stdscr, y: int, title: str, color_pair: int):
|
||||||
|
"""Draw a section header."""
|
||||||
|
height, width = stdscr.getmaxyx()
|
||||||
|
|
||||||
|
if y >= height:
|
||||||
|
return
|
||||||
|
|
||||||
|
_safe_addstr(stdscr, y, 2, title, curses.color_pair(color_pair) | curses.A_BOLD)
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr,
|
||||||
|
y,
|
||||||
|
len(title) + 3,
|
||||||
|
"─" * (width - len(title) - 5),
|
||||||
|
curses.color_pair(color_pair) | curses.A_BOLD,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _calculate_rates(history: List) -> dict:
|
||||||
|
"""Calculate per-second rates from history."""
|
||||||
|
if len(history) < 2:
|
||||||
|
return {
|
||||||
|
"syscalls_per_sec": 0.0,
|
||||||
|
"rx_bytes_per_sec": 0.0,
|
||||||
|
"tx_bytes_per_sec": 0.0,
|
||||||
|
"rx_pkts_per_sec": 0.0,
|
||||||
|
"tx_pkts_per_sec": 0.0,
|
||||||
|
"read_bytes_per_sec": 0.0,
|
||||||
|
"write_bytes_per_sec": 0.0,
|
||||||
|
"read_ops_per_sec": 0.0,
|
||||||
|
"write_ops_per_sec": 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate delta between last two samples
|
||||||
|
recent = history[-1]
|
||||||
|
previous = history[-2]
|
||||||
|
time_delta = recent.timestamp - previous.timestamp
|
||||||
|
|
||||||
|
if time_delta <= 0:
|
||||||
|
time_delta = 1.0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"syscalls_per_sec": (recent.syscall_count - previous.syscall_count)
|
||||||
|
/ time_delta,
|
||||||
|
"rx_bytes_per_sec": (recent.rx_bytes - previous.rx_bytes) / time_delta,
|
||||||
|
"tx_bytes_per_sec": (recent.tx_bytes - previous.tx_bytes) / time_delta,
|
||||||
|
"rx_pkts_per_sec": (recent.rx_packets - previous.rx_packets) / time_delta,
|
||||||
|
"tx_pkts_per_sec": (recent.tx_packets - previous.tx_packets) / time_delta,
|
||||||
|
"read_bytes_per_sec": (recent.read_bytes - previous.read_bytes) / time_delta,
|
||||||
|
"write_bytes_per_sec": (recent.write_bytes - previous.write_bytes) / time_delta,
|
||||||
|
"read_ops_per_sec": (recent.read_ops - previous.read_ops) / time_delta,
|
||||||
|
"write_ops_per_sec": (recent.write_ops - previous.write_ops) / time_delta,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _format_bytes(bytes_val: float) -> str:
|
||||||
|
"""Format bytes into human-readable string."""
|
||||||
|
if bytes_val < 0:
|
||||||
|
bytes_val = 0
|
||||||
|
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
||||||
|
if bytes_val < 1024.0:
|
||||||
|
return f"{bytes_val:.1f}{unit}"
|
||||||
|
bytes_val /= 1024.0
|
||||||
|
return f"{bytes_val:.1f}PB"
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_bar_graph_enhanced(
|
||||||
|
stdscr,
|
||||||
|
y: int,
|
||||||
|
x: int,
|
||||||
|
width: int,
|
||||||
|
height: int,
|
||||||
|
data: List[float],
|
||||||
|
color_pair: int,
|
||||||
|
):
|
||||||
|
"""Draw an enhanced bar graph with axis and scale."""
|
||||||
|
screen_height, screen_width = stdscr.getmaxyx()
|
||||||
|
|
||||||
|
if not data or width < 2 or y + height >= screen_height:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Calculate statistics
|
||||||
|
max_val = max(data) if max(data) > 0 else 1
|
||||||
|
min_val = min(data)
|
||||||
|
avg_val = sum(data) / len(data)
|
||||||
|
|
||||||
|
# Take last 'width - 12' data points (leave room for Y-axis)
|
||||||
|
graph_width = max(1, width - 12)
|
||||||
|
recent_data = data[-graph_width:] if len(data) > graph_width else data
|
||||||
|
|
||||||
|
# Draw Y-axis labels (with bounds checking)
|
||||||
|
if y < screen_height:
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr, y, x, f"│{_format_bytes(max_val):>9}", curses.color_pair(7)
|
||||||
|
)
|
||||||
|
if y + height // 2 < screen_height:
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr,
|
||||||
|
y + height // 2,
|
||||||
|
x,
|
||||||
|
f"│{_format_bytes(avg_val):>9}",
|
||||||
|
curses.color_pair(7),
|
||||||
|
)
|
||||||
|
if y + height - 1 < screen_height:
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr,
|
||||||
|
y + height - 1,
|
||||||
|
x,
|
||||||
|
f"│{_format_bytes(min_val):>9}",
|
||||||
|
curses.color_pair(7),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Draw bars
|
||||||
|
for row in range(height):
|
||||||
|
if y + row >= screen_height:
|
||||||
|
break
|
||||||
|
|
||||||
|
threshold = (height - row) / height
|
||||||
|
bar_line = ""
|
||||||
|
|
||||||
|
for val in recent_data:
|
||||||
|
normalized = val / max_val if max_val > 0 else 0
|
||||||
|
if normalized >= threshold:
|
||||||
|
bar_line += "█"
|
||||||
|
elif normalized >= threshold - 0.15:
|
||||||
|
bar_line += "▓"
|
||||||
|
elif normalized >= threshold - 0.35:
|
||||||
|
bar_line += "▒"
|
||||||
|
elif normalized >= threshold - 0.5:
|
||||||
|
bar_line += "░"
|
||||||
|
else:
|
||||||
|
bar_line += " "
|
||||||
|
|
||||||
|
_safe_addstr(stdscr, y + row, x + 11, bar_line, color_pair)
|
||||||
|
|
||||||
|
# Draw X-axis
|
||||||
|
if y + height < screen_height:
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr,
|
||||||
|
y + height,
|
||||||
|
x + 10,
|
||||||
|
"├" + "─" * len(recent_data),
|
||||||
|
curses.color_pair(7),
|
||||||
|
)
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr,
|
||||||
|
y + height,
|
||||||
|
x + 10 + len(recent_data),
|
||||||
|
"→ time",
|
||||||
|
curses.color_pair(7),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_labeled_graph(
|
||||||
|
stdscr,
|
||||||
|
y: int,
|
||||||
|
x: int,
|
||||||
|
width: int,
|
||||||
|
height: int,
|
||||||
|
label: str,
|
||||||
|
rate: str,
|
||||||
|
detail: str,
|
||||||
|
data: List[float],
|
||||||
|
color_pair: int,
|
||||||
|
description: str,
|
||||||
|
):
|
||||||
|
"""Draw a graph with labels and legend."""
|
||||||
|
screen_height, screen_width = stdscr.getmaxyx()
|
||||||
|
|
||||||
|
if y >= screen_height or y + height + 2 >= screen_height:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Header with metrics
|
||||||
|
_safe_addstr(stdscr, y, x, label, curses.color_pair(1) | curses.A_BOLD)
|
||||||
|
_safe_addstr(stdscr, y, x + len(label) + 2, rate, curses.color_pair(2))
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr, y, x + len(label) + len(rate) + 4, detail, curses.color_pair(7)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Draw the graph
|
||||||
|
if len(data) > 1:
|
||||||
|
_draw_bar_graph_enhanced(stdscr, y + 1, x, width, height, data, color_pair)
|
||||||
|
else:
|
||||||
|
_safe_addstr(stdscr, y + 2, x + 2, "Collecting data...", curses.color_pair(7))
|
||||||
|
|
||||||
|
# Graph legend
|
||||||
|
if y + height + 1 < screen_height:
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr, y + height + 1, x, f"└─ {description}", curses.color_pair(7)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ContainerMonitorTUI:
|
||||||
|
"""TUI for container monitoring with cgroup selection and live graphs."""
|
||||||
|
|
||||||
|
def __init__(self, collector: ContainerDataCollector):
|
||||||
|
self.collector = collector
|
||||||
|
self.selected_cgroup: Optional[int] = None
|
||||||
|
self.current_screen = "selection" # "selection" or "monitoring"
|
||||||
|
self.selected_index = 0
|
||||||
|
self.scroll_offset = 0
|
||||||
|
self.web_dashboard = None
|
||||||
|
self.web_thread = None
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Run the TUI application."""
|
||||||
|
curses.wrapper(self._main_loop)
|
||||||
|
|
||||||
|
def _main_loop(self, stdscr):
|
||||||
|
"""Main curses loop."""
|
||||||
|
# Configure curses
|
||||||
|
curses.curs_set(0) # Hide cursor
|
||||||
|
stdscr.nodelay(True) # Non-blocking input
|
||||||
|
stdscr.timeout(100) # Refresh every 100ms
|
||||||
|
|
||||||
|
# Initialize colors
|
||||||
|
curses.start_color()
|
||||||
|
curses.init_pair(1, curses.COLOR_CYAN, curses.COLOR_BLACK)
|
||||||
|
curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK)
|
||||||
|
curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK)
|
||||||
|
curses.init_pair(4, curses.COLOR_RED, curses.COLOR_BLACK)
|
||||||
|
curses.init_pair(5, curses.COLOR_MAGENTA, curses.COLOR_BLACK)
|
||||||
|
curses.init_pair(6, curses.COLOR_WHITE, curses.COLOR_BLUE)
|
||||||
|
curses.init_pair(7, curses.COLOR_BLUE, curses.COLOR_BLACK)
|
||||||
|
curses.init_pair(8, curses.COLOR_WHITE, curses.COLOR_CYAN)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
stdscr.clear()
|
||||||
|
|
||||||
|
try:
|
||||||
|
height, width = stdscr.getmaxyx()
|
||||||
|
|
||||||
|
# Check minimum terminal size
|
||||||
|
if height < 25 or width < 80:
|
||||||
|
msg = "Terminal too small! Minimum: 80x25"
|
||||||
|
stdscr.attron(curses.color_pair(4) | curses.A_BOLD)
|
||||||
|
stdscr.addstr(
|
||||||
|
height // 2, max(0, (width - len(msg)) // 2), msg[: width - 1]
|
||||||
|
)
|
||||||
|
stdscr.attroff(curses.color_pair(4) | curses.A_BOLD)
|
||||||
|
stdscr.refresh()
|
||||||
|
key = stdscr.getch()
|
||||||
|
if key == ord("q") or key == ord("Q"):
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self.current_screen == "selection":
|
||||||
|
self._draw_selection_screen(stdscr)
|
||||||
|
elif self.current_screen == "monitoring":
|
||||||
|
self._draw_monitoring_screen(stdscr)
|
||||||
|
|
||||||
|
stdscr.refresh()
|
||||||
|
|
||||||
|
# Handle input
|
||||||
|
key = stdscr.getch()
|
||||||
|
if key != -1:
|
||||||
|
if not self._handle_input(key, stdscr):
|
||||||
|
break # Exit requested
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
break
|
||||||
|
except curses.error:
|
||||||
|
# Curses error - likely terminal too small, just continue
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
# Show error briefly
|
||||||
|
height, width = stdscr.getmaxyx()
|
||||||
|
error_msg = f"Error: {str(e)[: width - 10]}"
|
||||||
|
stdscr.addstr(0, 0, error_msg[: width - 1])
|
||||||
|
stdscr.refresh()
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
def _draw_selection_screen(self, stdscr):
|
||||||
|
"""Draw the cgroup selection screen."""
|
||||||
|
height, width = stdscr.getmaxyx()
|
||||||
|
|
||||||
|
# Draw fancy header box
|
||||||
|
_draw_fancy_header(stdscr, "🐳 CONTAINER MONITOR", "Select a Cgroup to Monitor")
|
||||||
|
|
||||||
|
# Instructions
|
||||||
|
instructions = (
|
||||||
|
"↑↓: Navigate | ENTER: Select | w: Web Mode | q: Quit | r: Refresh"
|
||||||
|
)
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr,
|
||||||
|
3,
|
||||||
|
max(0, (width - len(instructions)) // 2),
|
||||||
|
instructions,
|
||||||
|
curses.color_pair(3),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get cgroups
|
||||||
|
cgroups = self.collector.get_all_cgroups()
|
||||||
|
|
||||||
|
if not cgroups:
|
||||||
|
msg = "No cgroups found. Waiting for activity..."
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr,
|
||||||
|
height // 2,
|
||||||
|
max(0, (width - len(msg)) // 2),
|
||||||
|
msg,
|
||||||
|
curses.color_pair(4),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Sort cgroups by name
|
||||||
|
cgroups.sort(key=lambda c: c.name)
|
||||||
|
|
||||||
|
# Adjust selection bounds
|
||||||
|
if self.selected_index >= len(cgroups):
|
||||||
|
self.selected_index = len(cgroups) - 1
|
||||||
|
if self.selected_index < 0:
|
||||||
|
self.selected_index = 0
|
||||||
|
|
||||||
|
# Calculate visible range
|
||||||
|
list_height = max(1, height - 8)
|
||||||
|
if self.selected_index < self.scroll_offset:
|
||||||
|
self.scroll_offset = self.selected_index
|
||||||
|
elif self.selected_index >= self.scroll_offset + list_height:
|
||||||
|
self.scroll_offset = self.selected_index - list_height + 1
|
||||||
|
|
||||||
|
# Calculate max name length and ID width for alignment
|
||||||
|
max_name_len = min(50, max(len(cg.name) for cg in cgroups))
|
||||||
|
max_id_len = max(len(str(cg.id)) for cg in cgroups)
|
||||||
|
|
||||||
|
# Draw cgroup list with fancy borders
|
||||||
|
start_y = 5
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr, start_y, 2, "╔" + "═" * (width - 6) + "╗", curses.color_pair(1)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Header row
|
||||||
|
header = f" {'CGROUP NAME':<{max_name_len}} │ {'ID':>{max_id_len}} "
|
||||||
|
_safe_addstr(stdscr, start_y + 1, 2, "║", curses.color_pair(1))
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr, start_y + 1, 3, header, curses.color_pair(1) | curses.A_BOLD
|
||||||
|
)
|
||||||
|
_safe_addstr(stdscr, start_y + 1, width - 3, "║", curses.color_pair(1))
|
||||||
|
|
||||||
|
# Separator
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr, start_y + 2, 2, "╟" + "─" * (width - 6) + "╢", curses.color_pair(1)
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in range(list_height):
|
||||||
|
idx = self.scroll_offset + i
|
||||||
|
y = start_y + 3 + i
|
||||||
|
|
||||||
|
if y >= height - 2:
|
||||||
|
break
|
||||||
|
|
||||||
|
_safe_addstr(stdscr, y, 2, "║", curses.color_pair(1))
|
||||||
|
_safe_addstr(stdscr, y, width - 3, "║", curses.color_pair(1))
|
||||||
|
|
||||||
|
if idx >= len(cgroups):
|
||||||
|
continue
|
||||||
|
|
||||||
|
cgroup = cgroups[idx]
|
||||||
|
|
||||||
|
# Truncate name if too long
|
||||||
|
display_name = (
|
||||||
|
cgroup.name
|
||||||
|
if len(cgroup.name) <= max_name_len
|
||||||
|
else cgroup.name[: max_name_len - 3] + "..."
|
||||||
|
)
|
||||||
|
|
||||||
|
if idx == self.selected_index:
|
||||||
|
# Highlight selected with proper alignment
|
||||||
|
line = f" ► {display_name:<{max_name_len}} │ {cgroup.id:>{max_id_len}} "
|
||||||
|
_safe_addstr(stdscr, y, 3, line, curses.color_pair(8) | curses.A_BOLD)
|
||||||
|
else:
|
||||||
|
line = f" {display_name:<{max_name_len}} │ {cgroup.id:>{max_id_len}} "
|
||||||
|
_safe_addstr(stdscr, y, 3, line, curses.color_pair(7))
|
||||||
|
|
||||||
|
# Bottom border
|
||||||
|
bottom_y = min(start_y + 3 + list_height, height - 3)
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr, bottom_y, 2, "╚" + "═" * (width - 6) + "╝", curses.color_pair(1)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Footer
|
||||||
|
footer = f"Total: {len(cgroups)} cgroups"
|
||||||
|
if len(cgroups) > list_height:
|
||||||
|
footer += f" │ Showing {self.scroll_offset + 1}-{min(self.scroll_offset + list_height, len(cgroups))}"
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr,
|
||||||
|
height - 2,
|
||||||
|
max(0, (width - len(footer)) // 2),
|
||||||
|
footer,
|
||||||
|
curses.color_pair(1),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _draw_monitoring_screen(self, stdscr):
|
||||||
|
"""Draw the monitoring screen for selected cgroup."""
|
||||||
|
height, width = stdscr.getmaxyx()
|
||||||
|
|
||||||
|
if self.selected_cgroup is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get current stats
|
||||||
|
stats = self.collector.get_stats_for_cgroup(self.selected_cgroup)
|
||||||
|
history = self.collector.get_history(self.selected_cgroup)
|
||||||
|
|
||||||
|
# Draw fancy header
|
||||||
|
_draw_fancy_header(
|
||||||
|
stdscr, f"📊 {stats.cgroup_name[:40]}", "Live Performance Metrics"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Instructions
|
||||||
|
instructions = "ESC/b: Back to List | w: Web Mode | q: Quit"
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr,
|
||||||
|
3,
|
||||||
|
max(0, (width - len(instructions)) // 2),
|
||||||
|
instructions,
|
||||||
|
curses.color_pair(3),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Calculate metrics for rate display
|
||||||
|
rates = _calculate_rates(history)
|
||||||
|
|
||||||
|
y = 5
|
||||||
|
|
||||||
|
# Syscall count in a fancy box
|
||||||
|
if y + 4 < height:
|
||||||
|
_draw_metric_box(
|
||||||
|
stdscr,
|
||||||
|
y,
|
||||||
|
2,
|
||||||
|
min(width - 4, 80),
|
||||||
|
"⚡ SYSTEM CALLS",
|
||||||
|
f"{stats.syscall_count:,}",
|
||||||
|
f"Rate: {rates['syscalls_per_sec']:.1f}/sec",
|
||||||
|
curses.color_pair(5),
|
||||||
|
)
|
||||||
|
y += 4
|
||||||
|
|
||||||
|
# Network I/O Section
|
||||||
|
if y + 8 < height:
|
||||||
|
_draw_section_header(stdscr, y, "🌐 NETWORK I/O", 1)
|
||||||
|
y += 1
|
||||||
|
|
||||||
|
# RX graph
|
||||||
|
rx_label = f"RX: {_format_bytes(stats.rx_bytes)}"
|
||||||
|
rx_rate = f"{_format_bytes(rates['rx_bytes_per_sec'])}/s"
|
||||||
|
rx_pkts = f"{stats.rx_packets:,} pkts ({rates['rx_pkts_per_sec']:.1f}/s)"
|
||||||
|
|
||||||
|
_draw_labeled_graph(
|
||||||
|
stdscr,
|
||||||
|
y,
|
||||||
|
2,
|
||||||
|
width - 4,
|
||||||
|
4,
|
||||||
|
rx_label,
|
||||||
|
rx_rate,
|
||||||
|
rx_pkts,
|
||||||
|
[s.rx_bytes for s in history],
|
||||||
|
curses.color_pair(2),
|
||||||
|
"Received Traffic (last 100 samples)",
|
||||||
|
)
|
||||||
|
y += 6
|
||||||
|
|
||||||
|
# TX graph
|
||||||
|
if y + 8 < height:
|
||||||
|
tx_label = f"TX: {_format_bytes(stats.tx_bytes)}"
|
||||||
|
tx_rate = f"{_format_bytes(rates['tx_bytes_per_sec'])}/s"
|
||||||
|
tx_pkts = f"{stats.tx_packets:,} pkts ({rates['tx_pkts_per_sec']:.1f}/s)"
|
||||||
|
|
||||||
|
_draw_labeled_graph(
|
||||||
|
stdscr,
|
||||||
|
y,
|
||||||
|
2,
|
||||||
|
width - 4,
|
||||||
|
4,
|
||||||
|
tx_label,
|
||||||
|
tx_rate,
|
||||||
|
tx_pkts,
|
||||||
|
[s.tx_bytes for s in history],
|
||||||
|
curses.color_pair(3),
|
||||||
|
"Transmitted Traffic (last 100 samples)",
|
||||||
|
)
|
||||||
|
y += 6
|
||||||
|
|
||||||
|
# File I/O Section
|
||||||
|
if y + 8 < height:
|
||||||
|
_draw_section_header(stdscr, y, "💾 FILE I/O", 1)
|
||||||
|
y += 1
|
||||||
|
|
||||||
|
# Read graph
|
||||||
|
read_label = f"READ: {_format_bytes(stats.read_bytes)}"
|
||||||
|
read_rate = f"{_format_bytes(rates['read_bytes_per_sec'])}/s"
|
||||||
|
read_ops = f"{stats.read_ops:,} ops ({rates['read_ops_per_sec']:.1f}/s)"
|
||||||
|
|
||||||
|
_draw_labeled_graph(
|
||||||
|
stdscr,
|
||||||
|
y,
|
||||||
|
2,
|
||||||
|
width - 4,
|
||||||
|
4,
|
||||||
|
read_label,
|
||||||
|
read_rate,
|
||||||
|
read_ops,
|
||||||
|
[s.read_bytes for s in history],
|
||||||
|
curses.color_pair(4),
|
||||||
|
"Read Operations (last 100 samples)",
|
||||||
|
)
|
||||||
|
y += 6
|
||||||
|
|
||||||
|
# Write graph
|
||||||
|
if y + 8 < height:
|
||||||
|
write_label = f"WRITE: {_format_bytes(stats.write_bytes)}"
|
||||||
|
write_rate = f"{_format_bytes(rates['write_bytes_per_sec'])}/s"
|
||||||
|
write_ops = f"{stats.write_ops:,} ops ({rates['write_ops_per_sec']:.1f}/s)"
|
||||||
|
|
||||||
|
_draw_labeled_graph(
|
||||||
|
stdscr,
|
||||||
|
y,
|
||||||
|
2,
|
||||||
|
width - 4,
|
||||||
|
4,
|
||||||
|
write_label,
|
||||||
|
write_rate,
|
||||||
|
write_ops,
|
||||||
|
[s.write_bytes for s in history],
|
||||||
|
curses.color_pair(5),
|
||||||
|
"Write Operations (last 100 samples)",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _launch_web_mode(self, stdscr):
|
||||||
|
"""Launch web dashboard mode."""
|
||||||
|
height, width = stdscr.getmaxyx()
|
||||||
|
|
||||||
|
# Show transition message
|
||||||
|
stdscr.clear()
|
||||||
|
|
||||||
|
msg1 = "🌐 LAUNCHING WEB DASHBOARD"
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr,
|
||||||
|
height // 2 - 2,
|
||||||
|
max(0, (width - len(msg1)) // 2),
|
||||||
|
msg1,
|
||||||
|
curses.color_pair(6) | curses.A_BOLD,
|
||||||
|
)
|
||||||
|
|
||||||
|
msg2 = "Server starting at http://localhost:8050"
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr,
|
||||||
|
height // 2,
|
||||||
|
max(0, (width - len(msg2)) // 2),
|
||||||
|
msg2,
|
||||||
|
curses.color_pair(2),
|
||||||
|
)
|
||||||
|
|
||||||
|
msg3 = "Press 'q' to stop web server and return to TUI"
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr,
|
||||||
|
height // 2 + 2,
|
||||||
|
max(0, (width - len(msg3)) // 2),
|
||||||
|
msg3,
|
||||||
|
curses.color_pair(3),
|
||||||
|
)
|
||||||
|
|
||||||
|
stdscr.refresh()
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create and start web dashboard
|
||||||
|
self.web_dashboard = WebDashboard(
|
||||||
|
self.collector, selected_cgroup=self.selected_cgroup
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start in background thread
|
||||||
|
self.web_thread = threading.Thread(
|
||||||
|
target=self.web_dashboard.run, daemon=True
|
||||||
|
)
|
||||||
|
self.web_thread.start()
|
||||||
|
|
||||||
|
time.sleep(2) # Give server time to start
|
||||||
|
|
||||||
|
# Wait for user to press 'q' to return
|
||||||
|
msg4 = "Web dashboard running at http://localhost:8050"
|
||||||
|
msg5 = "Press 'q' to return to TUI"
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr,
|
||||||
|
height // 2 + 4,
|
||||||
|
max(0, (width - len(msg4)) // 2),
|
||||||
|
msg4,
|
||||||
|
curses.color_pair(1) | curses.A_BOLD,
|
||||||
|
)
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr,
|
||||||
|
height // 2 + 5,
|
||||||
|
max(0, (width - len(msg5)) // 2),
|
||||||
|
msg5,
|
||||||
|
curses.color_pair(3) | curses.A_BOLD,
|
||||||
|
)
|
||||||
|
stdscr.refresh()
|
||||||
|
|
||||||
|
stdscr.nodelay(False) # Blocking mode
|
||||||
|
while True:
|
||||||
|
key = stdscr.getch()
|
||||||
|
if key == ord("q") or key == ord("Q"):
|
||||||
|
break
|
||||||
|
|
||||||
|
# Stop web server
|
||||||
|
if self.web_dashboard:
|
||||||
|
self.web_dashboard.stop()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Error starting web dashboard: {str(e)}"
|
||||||
|
_safe_addstr(
|
||||||
|
stdscr,
|
||||||
|
height // 2 + 4,
|
||||||
|
max(0, (width - len(error_msg)) // 2),
|
||||||
|
error_msg,
|
||||||
|
curses.color_pair(4),
|
||||||
|
)
|
||||||
|
stdscr.refresh()
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# Restore TUI settings
|
||||||
|
stdscr.nodelay(True)
|
||||||
|
stdscr.timeout(100)
|
||||||
|
|
||||||
|
def _handle_input(self, key: int, stdscr) -> bool:
|
||||||
|
"""Handle keyboard input. Returns False to exit."""
|
||||||
|
if key == ord("q") or key == ord("Q"):
|
||||||
|
return False # Exit
|
||||||
|
|
||||||
|
if key == ord("w") or key == ord("W"):
|
||||||
|
# Launch web mode
|
||||||
|
self._launch_web_mode(stdscr)
|
||||||
|
return True
|
||||||
|
|
||||||
|
if self.current_screen == "selection":
|
||||||
|
if key == curses.KEY_UP:
|
||||||
|
self.selected_index = max(0, self.selected_index - 1)
|
||||||
|
elif key == curses.KEY_DOWN:
|
||||||
|
cgroups = self.collector.get_all_cgroups()
|
||||||
|
self.selected_index = min(len(cgroups) - 1, self.selected_index + 1)
|
||||||
|
elif key == ord("\n") or key == curses.KEY_ENTER or key == 10:
|
||||||
|
# Select cgroup
|
||||||
|
cgroups = self.collector.get_all_cgroups()
|
||||||
|
if cgroups and 0 <= self.selected_index < len(cgroups):
|
||||||
|
cgroups.sort(key=lambda c: c.name)
|
||||||
|
self.selected_cgroup = cgroups[self.selected_index].id
|
||||||
|
self.current_screen = "monitoring"
|
||||||
|
elif key == ord("r") or key == ord("R"):
|
||||||
|
# Force refresh cache
|
||||||
|
self.collector._cgroup_cache_time = 0
|
||||||
|
|
||||||
|
elif self.current_screen == "monitoring":
|
||||||
|
if key == 27 or key == ord("b") or key == ord("B"): # ESC or 'b'
|
||||||
|
self.current_screen = "selection"
|
||||||
|
self.selected_cgroup = None
|
||||||
|
|
||||||
|
return True # Continue running
|
||||||
826
examples/container-monitor/web_dashboard.py
Normal file
826
examples/container-monitor/web_dashboard.py
Normal file
@ -0,0 +1,826 @@
|
|||||||
|
"""Beautiful web dashboard for container monitoring using Plotly Dash."""
|
||||||
|
|
||||||
|
import dash
|
||||||
|
from dash import dcc, html
|
||||||
|
from dash.dependencies import Input, Output
|
||||||
|
import plotly.graph_objects as go
|
||||||
|
from plotly.subplots import make_subplots
|
||||||
|
from typing import Optional
|
||||||
|
from data_collection import ContainerDataCollector
|
||||||
|
|
||||||
|
|
||||||
|
class WebDashboard:
|
||||||
|
"""Beautiful web dashboard for container monitoring."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
collector: ContainerDataCollector,
|
||||||
|
selected_cgroup: Optional[int] = None,
|
||||||
|
host: str = "0.0.0.0",
|
||||||
|
port: int = 8050,
|
||||||
|
):
|
||||||
|
self.collector = collector
|
||||||
|
self.selected_cgroup = selected_cgroup
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
|
||||||
|
# Suppress Dash dev tools and debug output
|
||||||
|
self.app = dash.Dash(
|
||||||
|
__name__,
|
||||||
|
title="pythonBPF Container Monitor",
|
||||||
|
suppress_callback_exceptions=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._setup_layout()
|
||||||
|
self._setup_callbacks()
|
||||||
|
self._running = False
|
||||||
|
|
||||||
|
def _setup_layout(self):
|
||||||
|
"""Create the dashboard layout."""
|
||||||
|
self.app.layout = html.Div(
|
||||||
|
[
|
||||||
|
# Futuristic Header with pythonBPF branding
|
||||||
|
html.Div(
|
||||||
|
[
|
||||||
|
html.Div(
|
||||||
|
[
|
||||||
|
html.Div(
|
||||||
|
[
|
||||||
|
html.Span(
|
||||||
|
"python",
|
||||||
|
style={
|
||||||
|
"fontSize": "52px",
|
||||||
|
"fontWeight": "300",
|
||||||
|
"color": "#00ff88",
|
||||||
|
"fontFamily": "'Courier New', monospace",
|
||||||
|
"textShadow": "0 0 20px rgba(0,255,136,0.5)",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
html.Span(
|
||||||
|
"BPF",
|
||||||
|
style={
|
||||||
|
"fontSize": "52px",
|
||||||
|
"fontWeight": "900",
|
||||||
|
"color": "#00d4ff",
|
||||||
|
"fontFamily": "'Courier New', monospace",
|
||||||
|
"textShadow": "0 0 20px rgba(0,212,255,0.5)",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style={"marginBottom": "5px"},
|
||||||
|
),
|
||||||
|
html.Div(
|
||||||
|
"CONTAINER PERFORMANCE MONITOR",
|
||||||
|
style={
|
||||||
|
"fontSize": "16px",
|
||||||
|
"letterSpacing": "8px",
|
||||||
|
"color": "#8899ff",
|
||||||
|
"fontWeight": "300",
|
||||||
|
"fontFamily": "'Courier New', monospace",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style={
|
||||||
|
"textAlign": "center",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
html.Div(
|
||||||
|
id="cgroup-name",
|
||||||
|
style={
|
||||||
|
"textAlign": "center",
|
||||||
|
"color": "#00ff88",
|
||||||
|
"fontSize": "20px",
|
||||||
|
"marginTop": "15px",
|
||||||
|
"fontFamily": "'Courier New', monospace",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"textShadow": "0 0 10px rgba(0,255,136,0.3)",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style={
|
||||||
|
"background": "linear-gradient(135deg, #0a0e27 0%, #1a1f3a 50%, #0a0e27 100%)",
|
||||||
|
"padding": "40px 20px",
|
||||||
|
"borderRadius": "0",
|
||||||
|
"marginBottom": "0",
|
||||||
|
"boxShadow": "0 10px 40px rgba(0,212,255,0.2)",
|
||||||
|
"border": "1px solid rgba(0,212,255,0.3)",
|
||||||
|
"borderTop": "3px solid #00d4ff",
|
||||||
|
"borderBottom": "3px solid #00ff88",
|
||||||
|
"position": "relative",
|
||||||
|
"overflow": "hidden",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
# Cgroup selector (if no cgroup selected)
|
||||||
|
html.Div(
|
||||||
|
[
|
||||||
|
html.Label(
|
||||||
|
"SELECT CGROUP:",
|
||||||
|
style={
|
||||||
|
"fontSize": "14px",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"color": "#00d4ff",
|
||||||
|
"marginRight": "15px",
|
||||||
|
"fontFamily": "'Courier New', monospace",
|
||||||
|
"letterSpacing": "2px",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
dcc.Dropdown(
|
||||||
|
id="cgroup-selector",
|
||||||
|
style={
|
||||||
|
"width": "600px",
|
||||||
|
"display": "inline-block",
|
||||||
|
"background": "#1a1f3a",
|
||||||
|
"border": "1px solid #00d4ff",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
id="selector-container",
|
||||||
|
style={
|
||||||
|
"textAlign": "center",
|
||||||
|
"marginTop": "30px",
|
||||||
|
"marginBottom": "30px",
|
||||||
|
"padding": "20px",
|
||||||
|
"background": "rgba(26,31,58,0.5)",
|
||||||
|
"borderRadius": "10px",
|
||||||
|
"border": "1px solid rgba(0,212,255,0.2)",
|
||||||
|
"display": "block" if self.selected_cgroup is None else "none",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
# Stats cards row
|
||||||
|
html.Div(
|
||||||
|
[
|
||||||
|
self._create_stat_card(
|
||||||
|
"syscall-card", "⚡ SYSCALLS", "#00ff88"
|
||||||
|
),
|
||||||
|
self._create_stat_card("network-card", "🌐 NETWORK", "#00d4ff"),
|
||||||
|
self._create_stat_card("file-card", "💾 FILE I/O", "#ff0088"),
|
||||||
|
],
|
||||||
|
style={
|
||||||
|
"display": "flex",
|
||||||
|
"justifyContent": "space-around",
|
||||||
|
"marginBottom": "30px",
|
||||||
|
"marginTop": "30px",
|
||||||
|
"gap": "25px",
|
||||||
|
"flexWrap": "wrap",
|
||||||
|
"padding": "0 20px",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
# Graphs container
|
||||||
|
html.Div(
|
||||||
|
[
|
||||||
|
# Network graphs
|
||||||
|
html.Div(
|
||||||
|
[
|
||||||
|
html.Div(
|
||||||
|
[
|
||||||
|
html.Span("🌐 ", style={"fontSize": "24px"}),
|
||||||
|
html.Span(
|
||||||
|
"NETWORK",
|
||||||
|
style={
|
||||||
|
"fontFamily": "'Courier New', monospace",
|
||||||
|
"letterSpacing": "3px",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
html.Span(
|
||||||
|
" I/O",
|
||||||
|
style={
|
||||||
|
"fontFamily": "'Courier New', monospace",
|
||||||
|
"letterSpacing": "3px",
|
||||||
|
"color": "#00d4ff",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style={
|
||||||
|
"color": "#ffffff",
|
||||||
|
"fontSize": "20px",
|
||||||
|
"borderBottom": "2px solid #00d4ff",
|
||||||
|
"paddingBottom": "15px",
|
||||||
|
"marginBottom": "25px",
|
||||||
|
"textShadow": "0 0 10px rgba(0,212,255,0.3)",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
dcc.Graph(
|
||||||
|
id="network-graph", style={"height": "400px"}
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style={
|
||||||
|
"background": "linear-gradient(135deg, #0a0e27 0%, #1a1f3a 100%)",
|
||||||
|
"padding": "30px",
|
||||||
|
"borderRadius": "15px",
|
||||||
|
"boxShadow": "0 8px 32px rgba(0,212,255,0.15)",
|
||||||
|
"marginBottom": "30px",
|
||||||
|
"border": "1px solid rgba(0,212,255,0.2)",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
# File I/O graphs
|
||||||
|
html.Div(
|
||||||
|
[
|
||||||
|
html.Div(
|
||||||
|
[
|
||||||
|
html.Span("💾 ", style={"fontSize": "24px"}),
|
||||||
|
html.Span(
|
||||||
|
"FILE",
|
||||||
|
style={
|
||||||
|
"fontFamily": "'Courier New', monospace",
|
||||||
|
"letterSpacing": "3px",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
html.Span(
|
||||||
|
" I/O",
|
||||||
|
style={
|
||||||
|
"fontFamily": "'Courier New', monospace",
|
||||||
|
"letterSpacing": "3px",
|
||||||
|
"color": "#ff0088",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style={
|
||||||
|
"color": "#ffffff",
|
||||||
|
"fontSize": "20px",
|
||||||
|
"borderBottom": "2px solid #ff0088",
|
||||||
|
"paddingBottom": "15px",
|
||||||
|
"marginBottom": "25px",
|
||||||
|
"textShadow": "0 0 10px rgba(255,0,136,0.3)",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
dcc.Graph(
|
||||||
|
id="file-io-graph", style={"height": "400px"}
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style={
|
||||||
|
"background": "linear-gradient(135deg, #0a0e27 0%, #1a1f3a 100%)",
|
||||||
|
"padding": "30px",
|
||||||
|
"borderRadius": "15px",
|
||||||
|
"boxShadow": "0 8px 32px rgba(255,0,136,0.15)",
|
||||||
|
"marginBottom": "30px",
|
||||||
|
"border": "1px solid rgba(255,0,136,0.2)",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
# Combined time series
|
||||||
|
html.Div(
|
||||||
|
[
|
||||||
|
html.Div(
|
||||||
|
[
|
||||||
|
html.Span("📈 ", style={"fontSize": "24px"}),
|
||||||
|
html.Span(
|
||||||
|
"REAL-TIME",
|
||||||
|
style={
|
||||||
|
"fontFamily": "'Courier New', monospace",
|
||||||
|
"letterSpacing": "3px",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
html.Span(
|
||||||
|
" METRICS",
|
||||||
|
style={
|
||||||
|
"fontFamily": "'Courier New', monospace",
|
||||||
|
"letterSpacing": "3px",
|
||||||
|
"color": "#00ff88",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style={
|
||||||
|
"color": "#ffffff",
|
||||||
|
"fontSize": "20px",
|
||||||
|
"borderBottom": "2px solid #00ff88",
|
||||||
|
"paddingBottom": "15px",
|
||||||
|
"marginBottom": "25px",
|
||||||
|
"textShadow": "0 0 10px rgba(0,255,136,0.3)",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
dcc.Graph(
|
||||||
|
id="timeseries-graph", style={"height": "500px"}
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style={
|
||||||
|
"background": "linear-gradient(135deg, #0a0e27 0%, #1a1f3a 100%)",
|
||||||
|
"padding": "30px",
|
||||||
|
"borderRadius": "15px",
|
||||||
|
"boxShadow": "0 8px 32px rgba(0,255,136,0.15)",
|
||||||
|
"border": "1px solid rgba(0,255,136,0.2)",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style={"padding": "0 20px"},
|
||||||
|
),
|
||||||
|
# Footer with pythonBPF branding
|
||||||
|
html.Div(
|
||||||
|
[
|
||||||
|
html.Div(
|
||||||
|
[
|
||||||
|
html.Span(
|
||||||
|
"Powered by ",
|
||||||
|
style={"color": "#8899ff", "fontSize": "12px"},
|
||||||
|
),
|
||||||
|
html.Span(
|
||||||
|
"pythonBPF",
|
||||||
|
style={
|
||||||
|
"color": "#00d4ff",
|
||||||
|
"fontSize": "14px",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"fontFamily": "'Courier New', monospace",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
html.Span(
|
||||||
|
" | eBPF Container Monitoring",
|
||||||
|
style={
|
||||||
|
"color": "#8899ff",
|
||||||
|
"fontSize": "12px",
|
||||||
|
"marginLeft": "10px",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
style={
|
||||||
|
"textAlign": "center",
|
||||||
|
"padding": "20px",
|
||||||
|
"marginTop": "40px",
|
||||||
|
"background": "linear-gradient(135deg, #0a0e27 0%, #1a1f3a 100%)",
|
||||||
|
"borderTop": "1px solid rgba(0,212,255,0.2)",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
# Auto-update interval
|
||||||
|
dcc.Interval(id="interval-component", interval=1000, n_intervals=0),
|
||||||
|
],
|
||||||
|
style={
|
||||||
|
"padding": "0",
|
||||||
|
"fontFamily": "'Segoe UI', 'Courier New', monospace",
|
||||||
|
"background": "linear-gradient(to bottom, #050813 0%, #0a0e27 100%)",
|
||||||
|
"minHeight": "100vh",
|
||||||
|
"margin": "0",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_stat_card(self, card_id: str, title: str, color: str):
|
||||||
|
"""Create a statistics card with futuristic styling."""
|
||||||
|
return html.Div(
|
||||||
|
[
|
||||||
|
html.H3(
|
||||||
|
title,
|
||||||
|
style={
|
||||||
|
"color": color,
|
||||||
|
"fontSize": "16px",
|
||||||
|
"marginBottom": "20px",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"fontFamily": "'Courier New', monospace",
|
||||||
|
"letterSpacing": "2px",
|
||||||
|
"textShadow": f"0 0 10px {color}50",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
html.Div(
|
||||||
|
[
|
||||||
|
html.Div(
|
||||||
|
id=f"{card_id}-value",
|
||||||
|
style={
|
||||||
|
"fontSize": "42px",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"color": "#ffffff",
|
||||||
|
"marginBottom": "10px",
|
||||||
|
"fontFamily": "'Courier New', monospace",
|
||||||
|
"textShadow": f"0 0 20px {color}40",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
html.Div(
|
||||||
|
id=f"{card_id}-rate",
|
||||||
|
style={
|
||||||
|
"fontSize": "14px",
|
||||||
|
"color": "#8899ff",
|
||||||
|
"fontFamily": "'Courier New', monospace",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style={
|
||||||
|
"flex": "1",
|
||||||
|
"minWidth": "280px",
|
||||||
|
"background": "linear-gradient(135deg, #0a0e27 0%, #1a1f3a 100%)",
|
||||||
|
"padding": "30px",
|
||||||
|
"borderRadius": "15px",
|
||||||
|
"boxShadow": f"0 8px 32px {color}20",
|
||||||
|
"border": f"1px solid {color}40",
|
||||||
|
"borderLeft": f"4px solid {color}",
|
||||||
|
"transition": "transform 0.3s, box-shadow 0.3s",
|
||||||
|
"position": "relative",
|
||||||
|
"overflow": "hidden",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _setup_callbacks(self):
|
||||||
|
"""Setup dashboard callbacks."""
|
||||||
|
|
||||||
|
@self.app.callback(
|
||||||
|
[Output("cgroup-selector", "options"), Output("cgroup-selector", "value")],
|
||||||
|
[Input("interval-component", "n_intervals")],
|
||||||
|
)
|
||||||
|
def update_cgroup_selector(n):
|
||||||
|
if self.selected_cgroup is not None:
|
||||||
|
return [], self.selected_cgroup
|
||||||
|
|
||||||
|
cgroups = self.collector.get_all_cgroups()
|
||||||
|
options = [
|
||||||
|
{"label": f"{cg.name} (ID: {cg.id})", "value": cg.id}
|
||||||
|
for cg in sorted(cgroups, key=lambda c: c.name)
|
||||||
|
]
|
||||||
|
value = options[0]["value"] if options else None
|
||||||
|
|
||||||
|
if value and self.selected_cgroup is None:
|
||||||
|
self.selected_cgroup = value
|
||||||
|
|
||||||
|
return options, self.selected_cgroup
|
||||||
|
|
||||||
|
@self.app.callback(
|
||||||
|
Output("cgroup-selector", "value", allow_duplicate=True),
|
||||||
|
[Input("cgroup-selector", "value")],
|
||||||
|
prevent_initial_call=True,
|
||||||
|
)
|
||||||
|
def select_cgroup(value):
|
||||||
|
if value:
|
||||||
|
self.selected_cgroup = value
|
||||||
|
return value
|
||||||
|
|
||||||
|
@self.app.callback(
|
||||||
|
[
|
||||||
|
Output("cgroup-name", "children"),
|
||||||
|
Output("syscall-card-value", "children"),
|
||||||
|
Output("syscall-card-rate", "children"),
|
||||||
|
Output("network-card-value", "children"),
|
||||||
|
Output("network-card-rate", "children"),
|
||||||
|
Output("file-card-value", "children"),
|
||||||
|
Output("file-card-rate", "children"),
|
||||||
|
Output("network-graph", "figure"),
|
||||||
|
Output("file-io-graph", "figure"),
|
||||||
|
Output("timeseries-graph", "figure"),
|
||||||
|
],
|
||||||
|
[Input("interval-component", "n_intervals")],
|
||||||
|
)
|
||||||
|
def update_dashboard(n):
|
||||||
|
if self.selected_cgroup is None:
|
||||||
|
empty_fig = self._create_empty_figure(
|
||||||
|
"Select a cgroup to begin monitoring"
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
"SELECT A CGROUP TO START",
|
||||||
|
"0",
|
||||||
|
"",
|
||||||
|
"0 B",
|
||||||
|
"",
|
||||||
|
"0 B",
|
||||||
|
"",
|
||||||
|
empty_fig,
|
||||||
|
empty_fig,
|
||||||
|
empty_fig,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
stats = self.collector.get_stats_for_cgroup(self.selected_cgroup)
|
||||||
|
history = self.collector.get_history(self.selected_cgroup)
|
||||||
|
rates = self._calculate_rates(history)
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"► {stats.cgroup_name}",
|
||||||
|
f"{stats.syscall_count:,}",
|
||||||
|
f"{rates['syscalls_per_sec']:.1f} calls/sec",
|
||||||
|
f"{self._format_bytes(stats.rx_bytes + stats.tx_bytes)}",
|
||||||
|
f"↓ {self._format_bytes(rates['rx_bytes_per_sec'])}/s ↑ {self._format_bytes(rates['tx_bytes_per_sec'])}/s",
|
||||||
|
f"{self._format_bytes(stats.read_bytes + stats.write_bytes)}",
|
||||||
|
f"R: {self._format_bytes(rates['read_bytes_per_sec'])}/s W: {self._format_bytes(rates['write_bytes_per_sec'])}/s",
|
||||||
|
self._create_network_graph(history),
|
||||||
|
self._create_file_io_graph(history),
|
||||||
|
self._create_timeseries_graph(history),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
empty_fig = self._create_empty_figure(f"Error: {str(e)}")
|
||||||
|
return (
|
||||||
|
"ERROR",
|
||||||
|
"0",
|
||||||
|
str(e),
|
||||||
|
"0 B",
|
||||||
|
"",
|
||||||
|
"0 B",
|
||||||
|
"",
|
||||||
|
empty_fig,
|
||||||
|
empty_fig,
|
||||||
|
empty_fig,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_empty_figure(self, message: str):
|
||||||
|
"""Create an empty figure with a message."""
|
||||||
|
fig = go.Figure()
|
||||||
|
fig.update_layout(
|
||||||
|
title=message,
|
||||||
|
template="plotly_dark",
|
||||||
|
paper_bgcolor="#0a0e27",
|
||||||
|
plot_bgcolor="#0a0e27",
|
||||||
|
font=dict(color="#8899ff", family="Courier New, monospace"),
|
||||||
|
)
|
||||||
|
return fig
|
||||||
|
|
||||||
|
def _create_network_graph(self, history):
|
||||||
|
"""Create network I/O graph with futuristic styling."""
|
||||||
|
if len(history) < 2:
|
||||||
|
return self._create_empty_figure("Collecting data...")
|
||||||
|
|
||||||
|
times = [i for i in range(len(history))]
|
||||||
|
rx_bytes = [s.rx_bytes for s in history]
|
||||||
|
tx_bytes = [s.tx_bytes for s in history]
|
||||||
|
|
||||||
|
fig = make_subplots(
|
||||||
|
rows=2,
|
||||||
|
cols=1,
|
||||||
|
subplot_titles=("RECEIVED (RX)", "TRANSMITTED (TX)"),
|
||||||
|
vertical_spacing=0.15,
|
||||||
|
)
|
||||||
|
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=times,
|
||||||
|
y=rx_bytes,
|
||||||
|
mode="lines",
|
||||||
|
name="RX",
|
||||||
|
fill="tozeroy",
|
||||||
|
line=dict(color="#00d4ff", width=3, shape="spline"),
|
||||||
|
fillcolor="rgba(0, 212, 255, 0.2)",
|
||||||
|
),
|
||||||
|
row=1,
|
||||||
|
col=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=times,
|
||||||
|
y=tx_bytes,
|
||||||
|
mode="lines",
|
||||||
|
name="TX",
|
||||||
|
fill="tozeroy",
|
||||||
|
line=dict(color="#00ff88", width=3, shape="spline"),
|
||||||
|
fillcolor="rgba(0, 255, 136, 0.2)",
|
||||||
|
),
|
||||||
|
row=2,
|
||||||
|
col=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
fig.update_xaxes(title_text="Time (samples)", row=2, col=1, color="#8899ff")
|
||||||
|
fig.update_yaxes(title_text="Bytes", row=1, col=1, color="#8899ff")
|
||||||
|
fig.update_yaxes(title_text="Bytes", row=2, col=1, color="#8899ff")
|
||||||
|
|
||||||
|
fig.update_layout(
|
||||||
|
height=400,
|
||||||
|
template="plotly_dark",
|
||||||
|
paper_bgcolor="rgba(0,0,0,0)",
|
||||||
|
plot_bgcolor="#0a0e27",
|
||||||
|
showlegend=False,
|
||||||
|
hovermode="x unified",
|
||||||
|
font=dict(family="Courier New, monospace", color="#8899ff"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return fig
|
||||||
|
|
||||||
|
def _create_file_io_graph(self, history):
|
||||||
|
"""Create file I/O graph with futuristic styling."""
|
||||||
|
if len(history) < 2:
|
||||||
|
return self._create_empty_figure("Collecting data...")
|
||||||
|
|
||||||
|
times = [i for i in range(len(history))]
|
||||||
|
read_bytes = [s.read_bytes for s in history]
|
||||||
|
write_bytes = [s.write_bytes for s in history]
|
||||||
|
|
||||||
|
fig = make_subplots(
|
||||||
|
rows=2,
|
||||||
|
cols=1,
|
||||||
|
subplot_titles=("READ OPERATIONS", "WRITE OPERATIONS"),
|
||||||
|
vertical_spacing=0.15,
|
||||||
|
)
|
||||||
|
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=times,
|
||||||
|
y=read_bytes,
|
||||||
|
mode="lines",
|
||||||
|
name="Read",
|
||||||
|
fill="tozeroy",
|
||||||
|
line=dict(color="#ff0088", width=3, shape="spline"),
|
||||||
|
fillcolor="rgba(255, 0, 136, 0.2)",
|
||||||
|
),
|
||||||
|
row=1,
|
||||||
|
col=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=times,
|
||||||
|
y=write_bytes,
|
||||||
|
mode="lines",
|
||||||
|
name="Write",
|
||||||
|
fill="tozeroy",
|
||||||
|
line=dict(color="#8844ff", width=3, shape="spline"),
|
||||||
|
fillcolor="rgba(136, 68, 255, 0.2)",
|
||||||
|
),
|
||||||
|
row=2,
|
||||||
|
col=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
fig.update_xaxes(title_text="Time (samples)", row=2, col=1, color="#8899ff")
|
||||||
|
fig.update_yaxes(title_text="Bytes", row=1, col=1, color="#8899ff")
|
||||||
|
fig.update_yaxes(title_text="Bytes", row=2, col=1, color="#8899ff")
|
||||||
|
|
||||||
|
fig.update_layout(
|
||||||
|
height=400,
|
||||||
|
template="plotly_dark",
|
||||||
|
paper_bgcolor="rgba(0,0,0,0)",
|
||||||
|
plot_bgcolor="#0a0e27",
|
||||||
|
showlegend=False,
|
||||||
|
hovermode="x unified",
|
||||||
|
font=dict(family="Courier New, monospace", color="#8899ff"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return fig
|
||||||
|
|
||||||
|
def _create_timeseries_graph(self, history):
|
||||||
|
"""Create combined time series graph with futuristic styling."""
|
||||||
|
if len(history) < 2:
|
||||||
|
return self._create_empty_figure("Collecting data...")
|
||||||
|
|
||||||
|
times = [i for i in range(len(history))]
|
||||||
|
|
||||||
|
fig = make_subplots(
|
||||||
|
rows=3,
|
||||||
|
cols=1,
|
||||||
|
subplot_titles=(
|
||||||
|
"SYSTEM CALLS",
|
||||||
|
"NETWORK TRAFFIC (Bytes)",
|
||||||
|
"FILE I/O (Bytes)",
|
||||||
|
),
|
||||||
|
vertical_spacing=0.1,
|
||||||
|
specs=[
|
||||||
|
[{"secondary_y": False}],
|
||||||
|
[{"secondary_y": True}],
|
||||||
|
[{"secondary_y": True}],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Syscalls
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=times,
|
||||||
|
y=[s.syscall_count for s in history],
|
||||||
|
mode="lines",
|
||||||
|
name="Syscalls",
|
||||||
|
line=dict(color="#00ff88", width=3, shape="spline"),
|
||||||
|
),
|
||||||
|
row=1,
|
||||||
|
col=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Network
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=times,
|
||||||
|
y=[s.rx_bytes for s in history],
|
||||||
|
mode="lines",
|
||||||
|
name="RX",
|
||||||
|
line=dict(color="#00d4ff", width=2, shape="spline"),
|
||||||
|
),
|
||||||
|
row=2,
|
||||||
|
col=1,
|
||||||
|
secondary_y=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=times,
|
||||||
|
y=[s.tx_bytes for s in history],
|
||||||
|
mode="lines",
|
||||||
|
name="TX",
|
||||||
|
line=dict(color="#00ff88", width=2, shape="spline", dash="dot"),
|
||||||
|
),
|
||||||
|
row=2,
|
||||||
|
col=1,
|
||||||
|
secondary_y=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# File I/O
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=times,
|
||||||
|
y=[s.read_bytes for s in history],
|
||||||
|
mode="lines",
|
||||||
|
name="Read",
|
||||||
|
line=dict(color="#ff0088", width=2, shape="spline"),
|
||||||
|
),
|
||||||
|
row=3,
|
||||||
|
col=1,
|
||||||
|
secondary_y=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=times,
|
||||||
|
y=[s.write_bytes for s in history],
|
||||||
|
mode="lines",
|
||||||
|
name="Write",
|
||||||
|
line=dict(color="#8844ff", width=2, shape="spline", dash="dot"),
|
||||||
|
),
|
||||||
|
row=3,
|
||||||
|
col=1,
|
||||||
|
secondary_y=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
fig.update_xaxes(title_text="Time (samples)", row=3, col=1, color="#8899ff")
|
||||||
|
fig.update_yaxes(title_text="Count", row=1, col=1, color="#8899ff")
|
||||||
|
fig.update_yaxes(
|
||||||
|
title_text="RX Bytes", row=2, col=1, secondary_y=False, color="#00d4ff"
|
||||||
|
)
|
||||||
|
fig.update_yaxes(
|
||||||
|
title_text="TX Bytes", row=2, col=1, secondary_y=True, color="#00ff88"
|
||||||
|
)
|
||||||
|
fig.update_yaxes(
|
||||||
|
title_text="Read Bytes", row=3, col=1, secondary_y=False, color="#ff0088"
|
||||||
|
)
|
||||||
|
fig.update_yaxes(
|
||||||
|
title_text="Write Bytes", row=3, col=1, secondary_y=True, color="#8844ff"
|
||||||
|
)
|
||||||
|
|
||||||
|
fig.update_layout(
|
||||||
|
height=500,
|
||||||
|
template="plotly_dark",
|
||||||
|
paper_bgcolor="rgba(0,0,0,0)",
|
||||||
|
plot_bgcolor="#0a0e27",
|
||||||
|
hovermode="x unified",
|
||||||
|
showlegend=True,
|
||||||
|
legend=dict(
|
||||||
|
orientation="h",
|
||||||
|
yanchor="bottom",
|
||||||
|
y=1.02,
|
||||||
|
xanchor="right",
|
||||||
|
x=1,
|
||||||
|
font=dict(color="#8899ff"),
|
||||||
|
),
|
||||||
|
font=dict(family="Courier New, monospace", color="#8899ff"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return fig
|
||||||
|
|
||||||
|
def _calculate_rates(self, history):
|
||||||
|
"""Calculate rates from history."""
|
||||||
|
if len(history) < 2:
|
||||||
|
return {
|
||||||
|
"syscalls_per_sec": 0.0,
|
||||||
|
"rx_bytes_per_sec": 0.0,
|
||||||
|
"tx_bytes_per_sec": 0.0,
|
||||||
|
"read_bytes_per_sec": 0.0,
|
||||||
|
"write_bytes_per_sec": 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
recent = history[-1]
|
||||||
|
previous = history[-2]
|
||||||
|
time_delta = recent.timestamp - previous.timestamp
|
||||||
|
|
||||||
|
if time_delta <= 0:
|
||||||
|
time_delta = 1.0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"syscalls_per_sec": max(
|
||||||
|
0, (recent.syscall_count - previous.syscall_count) / time_delta
|
||||||
|
),
|
||||||
|
"rx_bytes_per_sec": max(
|
||||||
|
0, (recent.rx_bytes - previous.rx_bytes) / time_delta
|
||||||
|
),
|
||||||
|
"tx_bytes_per_sec": max(
|
||||||
|
0, (recent.tx_bytes - previous.tx_bytes) / time_delta
|
||||||
|
),
|
||||||
|
"read_bytes_per_sec": max(
|
||||||
|
0, (recent.read_bytes - previous.read_bytes) / time_delta
|
||||||
|
),
|
||||||
|
"write_bytes_per_sec": max(
|
||||||
|
0, (recent.write_bytes - previous.write_bytes) / time_delta
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _format_bytes(self, bytes_val: float) -> str:
|
||||||
|
"""Format bytes into human-readable string."""
|
||||||
|
if bytes_val < 0:
|
||||||
|
bytes_val = 0
|
||||||
|
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
||||||
|
if bytes_val < 1024.0:
|
||||||
|
return f"{bytes_val:.2f} {unit}"
|
||||||
|
bytes_val /= 1024.0
|
||||||
|
return f"{bytes_val:.2f} PB"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Run the web dashboard."""
|
||||||
|
self._running = True
|
||||||
|
# Suppress Werkzeug logging
|
||||||
|
import logging
|
||||||
|
|
||||||
|
log = logging.getLogger("werkzeug")
|
||||||
|
log.setLevel(logging.ERROR)
|
||||||
|
|
||||||
|
self.app.run(debug=False, host=self.host, port=self.port, use_reloader=False)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop the web dashboard."""
|
||||||
|
self._running = False
|
||||||
@ -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,26 @@
|
|||||||
# This is what it is going to look like
|
from pythonbpf import bpf, section, bpfglobal, BPF, trace_pipe
|
||||||
# pylint: disable-all# type: ignore
|
from ctypes import c_void_p, c_int64
|
||||||
from pythonbpf.decorators import tracepoint, syscalls, bpfglobal, bpf
|
|
||||||
from ctypes import c_void_p, c_int32
|
# 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
|
@bpf
|
||||||
@tracepoint(syscalls.sys_clone)
|
@section("tracepoint/syscalls/sys_enter_execve")
|
||||||
def trace_clone(ctx: c_void_p) -> c_int32:
|
def hello_world(ctx: c_void_p) -> c_int64:
|
||||||
print("Hello, World!")
|
print("Hello, World!")
|
||||||
return c_int32(0)
|
return c_int64(0)
|
||||||
|
|
||||||
|
|
||||||
@bpf
|
@bpf
|
||||||
@bpfglobal
|
@bpfglobal
|
||||||
def LICENSE() -> str:
|
def LICENSE() -> str:
|
||||||
return "GPL"
|
return "GPL"
|
||||||
|
|
||||||
|
|
||||||
|
b = BPF()
|
||||||
|
b.load()
|
||||||
|
b.attach_all()
|
||||||
|
trace_pipe()
|
||||||
|
|||||||
29
examples/kprobes.py
Normal file
29
examples/kprobes.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from pythonbpf import bpf, section, bpfglobal, BPF, trace_pipe
|
||||||
|
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()
|
||||||
|
b.attach_all()
|
||||||
|
print("running")
|
||||||
|
trace_pipe()
|
||||||
@ -1,35 +0,0 @@
|
|||||||
from pythonbpf import bpf, section, bpfglobal, BPF
|
|
||||||
import sys
|
|
||||||
from ctypes import c_void_p, c_int64
|
|
||||||
|
|
||||||
# Instructions to how to run this program
|
|
||||||
# 1. Install PythonBPF: pip install pythonbpf
|
|
||||||
# 2. `sudo /path/to/venv/bin/python ./python-bpf/demo/pybpf0.py`
|
|
||||||
|
|
||||||
@bpf
|
|
||||||
@section("tracepoint/syscalls/sys_enter_execve")
|
|
||||||
def hello_world(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()
|
|
||||||
|
|
||||||
def main():
|
|
||||||
try:
|
|
||||||
with open("/sys/kernel/debug/tracing/trace_pipe", "r") as f:
|
|
||||||
for line in f:
|
|
||||||
sys.stdout.write(line)
|
|
||||||
sys.stdout.flush()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
except PermissionError:
|
|
||||||
sys.stderr.write("Need root privileges to read trace_pipe\n")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
from pythonbpf import bpf, map, section, bpfglobal, compile
|
|
||||||
from pythonbpf.helpers import XDP_PASS
|
|
||||||
from pythonbpf.maps import HashMap
|
|
||||||
|
|
||||||
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
|
|
||||||
# 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: c_void_p) -> 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()
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
from pythonbpf import *
|
|
||||||
from pylibbpf import *
|
|
||||||
import sys
|
|
||||||
from ctypes import c_void_p, c_int64, c_uint64
|
|
||||||
|
|
||||||
@bpf
|
|
||||||
@map
|
|
||||||
def last() -> HashMap:
|
|
||||||
return HashMap(key=c_uint64, value=c_uint64, max_entries=3)
|
|
||||||
|
|
||||||
@bpf
|
|
||||||
@section("tracepoint/syscalls/sys_enter_clone")
|
|
||||||
def do_trace(ctx: c_void_p) -> c_int64:
|
|
||||||
key = 0
|
|
||||||
tsp = last().lookup(key)
|
|
||||||
if tsp:
|
|
||||||
kt = ktime()
|
|
||||||
delta = (kt - tsp)
|
|
||||||
if delta < 1000000000:
|
|
||||||
time_ms = (delta // 1000000)
|
|
||||||
print(f"Clone syscall entered within last second, last {time_ms} ms ago")
|
|
||||||
last().delete(key)
|
|
||||||
else:
|
|
||||||
kt = ktime()
|
|
||||||
last().update(key, kt)
|
|
||||||
return c_int64(0)
|
|
||||||
|
|
||||||
@bpf
|
|
||||||
@bpfglobal
|
|
||||||
def LICENSE() -> str:
|
|
||||||
return "GPL"
|
|
||||||
|
|
||||||
b = BPF()
|
|
||||||
# autoattaches tracepoints
|
|
||||||
b.load_and_attach()
|
|
||||||
|
|
||||||
def main():
|
|
||||||
try:
|
|
||||||
with open("/sys/kernel/debug/tracing/trace_pipe", "r") as f:
|
|
||||||
for line in f:
|
|
||||||
sys.stdout.write(line)
|
|
||||||
sys.stdout.flush()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
except PermissionError:
|
|
||||||
sys.stderr.write("Need root privileges to read trace_pipe\n")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
import time
|
|
||||||
|
|
||||||
from pythonbpf import bpf, map, section, bpfglobal, BPF
|
|
||||||
from pythonbpf.helpers import pid
|
|
||||||
from pythonbpf.maps import HashMap
|
|
||||||
from pylibbpf import BpfMap
|
|
||||||
|
|
||||||
from ctypes import c_void_p, c_int64, c_uint64, c_int32
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
# Everything is done with Python only code and with the new pylibbpf library.
|
|
||||||
# Run `sudo /path/to/python/binary/ pybpf4.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:
|
|
||||||
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"
|
|
||||||
|
|
||||||
b = BPF()
|
|
||||||
b.load_and_attach()
|
|
||||||
hist = BpfMap(b, hist)
|
|
||||||
print("Recording")
|
|
||||||
time.sleep(10)
|
|
||||||
|
|
||||||
counts = list(hist.values())
|
|
||||||
x = 0
|
|
||||||
for key in hist.keys():
|
|
||||||
if hist[key] > 40:
|
|
||||||
x += 1
|
|
||||||
print(f"PID {key} called clone() >40 times")
|
|
||||||
|
|
||||||
print(f"Total PIDs with clone() >40 times: {x}")
|
|
||||||
plt.hist(counts, bins=20)
|
|
||||||
plt.xlabel("Clone calls per PID")
|
|
||||||
plt.ylabel("Number of processes that called clone() x times in last 10 seconds")
|
|
||||||
plt.title("x")
|
|
||||||
plt.show()
|
|
||||||
@ -1,8 +1,8 @@
|
|||||||
from pythonbpf import bpf, map, struct, section, bpfglobal, compile
|
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 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
|
@bpf
|
||||||
@ -23,13 +23,11 @@ def events() -> PerfEventArray:
|
|||||||
@section("tracepoint/syscalls/sys_enter_clone")
|
@section("tracepoint/syscalls/sys_enter_clone")
|
||||||
def hello(ctx: c_void_p) -> c_int32:
|
def hello(ctx: c_void_p) -> c_int32:
|
||||||
dataobj = data_t()
|
dataobj = data_t()
|
||||||
ts = ktime()
|
|
||||||
process_id = pid()
|
|
||||||
strobj = "hellohellohello"
|
strobj = "hellohellohello"
|
||||||
dataobj.pid = pid()
|
dataobj.pid = pid()
|
||||||
dataobj.ts = ktime()
|
dataobj.ts = ktime()
|
||||||
# dataobj.comm = strobj
|
# dataobj.comm = strobj
|
||||||
print(f"clone called at {dataobj.ts} by pid {dataobj.pid}, comm {strobj}")
|
print(f"clone called at {dataobj.ts} by pid{dataobj.pid}, comm {strobj}")
|
||||||
events.output(dataobj)
|
events.output(dataobj)
|
||||||
return c_int32(0)
|
return c_int32(0)
|
||||||
|
|
||||||
@ -1,15 +1,16 @@
|
|||||||
from pythonbpf import bpf, map, section, bpfglobal, compile
|
from pythonbpf import bpf, map, section, bpfglobal, compile
|
||||||
from pythonbpf.helpers import ktime
|
from pythonbpf.helper import ktime
|
||||||
from pythonbpf.maps import HashMap
|
from pythonbpf.maps import HashMap
|
||||||
|
|
||||||
from ctypes import c_void_p, c_int64, c_uint64
|
from ctypes import c_void_p, c_int64, c_uint64
|
||||||
|
|
||||||
# Instructions to how to run this program
|
# Instructions to how to run this program
|
||||||
# 1. Install PythonBPF: pip install pythonbpf
|
# 1. Install PythonBPF: pip install pythonbpf
|
||||||
# 2. Run the program: python demo/pybpf2.py
|
# 2. Run the program: python examples/sys_sync.py
|
||||||
# 3. Run the program with sudo: sudo examples/check.sh run demo/pybpf2.o
|
# 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.
|
# 4. Start a Python repl and `import os` and then keep entering `os.sync()` to see reponses.
|
||||||
|
|
||||||
|
|
||||||
@bpf
|
@bpf
|
||||||
@map
|
@map
|
||||||
def last() -> HashMap:
|
def last() -> HashMap:
|
||||||
@ -20,17 +21,17 @@ def last() -> HashMap:
|
|||||||
@section("tracepoint/syscalls/sys_enter_sync")
|
@section("tracepoint/syscalls/sys_enter_sync")
|
||||||
def do_trace(ctx: c_void_p) -> c_int64:
|
def do_trace(ctx: c_void_p) -> c_int64:
|
||||||
key = 0
|
key = 0
|
||||||
tsp = last().lookup(key)
|
tsp = last.lookup(key)
|
||||||
if tsp:
|
if tsp:
|
||||||
kt = ktime()
|
kt = ktime()
|
||||||
delta = (kt - tsp)
|
delta = kt - tsp
|
||||||
if delta < 1000000000:
|
if delta < 1000000000:
|
||||||
time_ms = (delta // 1000000)
|
time_ms = delta // 1000000
|
||||||
print(f"sync called within last second, last {time_ms} ms ago")
|
print(f"sync called within last second, last {time_ms} ms ago")
|
||||||
last().delete(key)
|
last.delete(key)
|
||||||
else:
|
else:
|
||||||
kt = ktime()
|
kt = ktime()
|
||||||
last().update(key, kt)
|
last.update(key, kt)
|
||||||
return c_int64(0)
|
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 import bpf, map, section, bpfglobal, compile, compile_to_ir
|
||||||
from pythonbpf.helpers import XDP_PASS
|
from pythonbpf.helper import XDP_PASS
|
||||||
from pythonbpf.maps import HashMap
|
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
|
# Instructions to how to run this program
|
||||||
# 1. Install PythonBPF: pip install pythonbpf
|
# 1. Install PythonBPF: pip install pythonbpf
|
||||||
# 2. Run the program: python demo/pybpf1.py
|
# 2. Run the program: python examples/xdp_pass.py
|
||||||
# 3. Run the program with sudo: sudo examples/check.sh run demo/pybpf1.o
|
# 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 ../demo/pybpf1.o tailscale0
|
# 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
|
# 5. send traffic through the device and observe effects
|
||||||
|
|
||||||
|
|
||||||
@bpf
|
@bpf
|
||||||
@map
|
@map
|
||||||
def count() -> HashMap:
|
def count() -> HashMap:
|
||||||
@ -22,20 +23,23 @@ def count() -> HashMap:
|
|||||||
def hello_world(ctx: c_void_p) -> c_int64:
|
def hello_world(ctx: c_void_p) -> c_int64:
|
||||||
key = 0
|
key = 0
|
||||||
one = 1
|
one = 1
|
||||||
prev = count().lookup(key)
|
prev = count.lookup(key)
|
||||||
if prev:
|
if prev:
|
||||||
prevval = prev + 1
|
prevval = prev + 1
|
||||||
print(f"count: {prevval}")
|
print(f"count: {prevval}")
|
||||||
count().update(key, prevval)
|
count.update(key, prevval)
|
||||||
return XDP_PASS
|
return XDP_PASS
|
||||||
else:
|
else:
|
||||||
count().update(key, one)
|
count.update(key, one)
|
||||||
|
|
||||||
return XDP_PASS
|
return XDP_PASS
|
||||||
|
|
||||||
|
|
||||||
@bpf
|
@bpf
|
||||||
@bpfglobal
|
@bpfglobal
|
||||||
def LICENSE() -> str:
|
def LICENSE() -> str:
|
||||||
return "GPL"
|
return "GPL"
|
||||||
|
|
||||||
|
|
||||||
|
compile_to_ir("xdp_pass.py", "xdp_pass.ll")
|
||||||
compile()
|
compile()
|
||||||
@ -4,22 +4,44 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pythonbpf"
|
name = "pythonbpf"
|
||||||
version = "0.1.3"
|
version = "0.1.8"
|
||||||
description = "Reduced Python frontend for eBPF"
|
description = "Reduced Python frontend for eBPF"
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "r41k0u", email="pragyanshchaturvedi18@gmail.com" },
|
{ name = "r41k0u", email="pragyanshchaturvedi18@gmail.com" },
|
||||||
{ name = "varun-r-mallya", email="varunrmallya@gmail.com" }
|
{ name = "varun-r-mallya", email="varunrmallya@gmail.com" }
|
||||||
]
|
]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 3 - Alpha",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Operating System :: POSIX :: Linux",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python",
|
||||||
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
|
"Topic :: System :: Operating System Kernels :: Linux",
|
||||||
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.10"
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"llvmlite",
|
"llvmlite>=0.45",
|
||||||
"astpretty",
|
"astpretty",
|
||||||
"pylibbpf"
|
"pylibbpf"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
docs = [
|
||||||
|
"sphinx>=7.0",
|
||||||
|
"myst-parser>=2.0",
|
||||||
|
"sphinx-rtd-theme>=2.0",
|
||||||
|
"sphinx-copybutton",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["."]
|
where = ["."]
|
||||||
include = ["pythonbpf*"]
|
include = ["pythonbpf*"]
|
||||||
|
|||||||
@ -1,2 +1,16 @@
|
|||||||
from .decorators import bpf, map, section, bpfglobal, struct
|
from .decorators import bpf, map, section, bpfglobal, struct
|
||||||
from .codegen import compile_to_ir, compile, BPF
|
from .codegen import compile_to_ir, compile, BPF
|
||||||
|
from .utils import trace_pipe, trace_fields
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"bpf",
|
||||||
|
"map",
|
||||||
|
"section",
|
||||||
|
"bpfglobal",
|
||||||
|
"struct",
|
||||||
|
"compile_to_ir",
|
||||||
|
"compile",
|
||||||
|
"BPF",
|
||||||
|
"trace_pipe",
|
||||||
|
"trace_fields",
|
||||||
|
]
|
||||||
|
|||||||
474
pythonbpf/allocation_pass.py
Normal file
474
pythonbpf/allocation_pass.py
Normal file
@ -0,0 +1,474 @@
|
|||||||
|
import ast
|
||||||
|
import logging
|
||||||
|
import ctypes
|
||||||
|
from llvmlite import ir
|
||||||
|
from .local_symbol import LocalSymbol
|
||||||
|
from pythonbpf.helper import HelperHandlerRegistry
|
||||||
|
from pythonbpf.vmlinux_parser.dependency_node import Field
|
||||||
|
from .expr import VmlinuxHandlerRegistry
|
||||||
|
from pythonbpf.type_deducer import ctypes_to_ir
|
||||||
|
from pythonbpf.maps import BPFMapType
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def create_targets_and_rvals(stmt):
|
||||||
|
"""Create lists of targets and right-hand values from an assignment statement."""
|
||||||
|
if isinstance(stmt.targets[0], ast.Tuple):
|
||||||
|
if not isinstance(stmt.value, ast.Tuple):
|
||||||
|
logger.warning("Mismatched multi-target assignment, skipping allocation")
|
||||||
|
return [], []
|
||||||
|
targets, rvals = stmt.targets[0].elts, stmt.value.elts
|
||||||
|
if len(targets) != len(rvals):
|
||||||
|
logger.warning("length of LHS != length of RHS, skipping allocation")
|
||||||
|
return [], []
|
||||||
|
return targets, rvals
|
||||||
|
return stmt.targets, [stmt.value]
|
||||||
|
|
||||||
|
|
||||||
|
def handle_assign_allocation(
|
||||||
|
builder, stmt, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||||
|
):
|
||||||
|
"""Handle memory allocation for assignment statements."""
|
||||||
|
|
||||||
|
logger.info(f"Handling assignment for allocation: {ast.dump(stmt)}")
|
||||||
|
|
||||||
|
# NOTE: Support multi-target assignments (e.g.: a, b = 1, 2)
|
||||||
|
targets, rvals = create_targets_and_rvals(stmt)
|
||||||
|
|
||||||
|
for target, rval in zip(targets, rvals):
|
||||||
|
# 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"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not isinstance(target, ast.Name):
|
||||||
|
logger.warning(
|
||||||
|
f"Unsupported assignment target type: {type(target).__name__}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
var_name = target.id
|
||||||
|
# Skip if already allocated
|
||||||
|
if var_name in local_sym_tab:
|
||||||
|
logger.debug(f"Variable {var_name} already allocated, skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Determine type and allocate based on rval
|
||||||
|
if isinstance(rval, ast.Call):
|
||||||
|
_allocate_for_call(
|
||||||
|
builder, var_name, rval, local_sym_tab, map_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)
|
||||||
|
elif isinstance(rval, ast.Name):
|
||||||
|
# Variable-to-variable assignment (b = a)
|
||||||
|
_allocate_for_name(builder, var_name, rval, local_sym_tab)
|
||||||
|
elif isinstance(rval, ast.Attribute):
|
||||||
|
# Struct field-to-variable assignment (a = dat.fld)
|
||||||
|
_allocate_for_attribute(
|
||||||
|
builder, var_name, rval, local_sym_tab, structs_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, map_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", "c_void_p"):
|
||||||
|
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]
|
||||||
|
if len(rval.args) == 0:
|
||||||
|
# Zero-arg constructor: allocate the struct itself
|
||||||
|
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:
|
||||||
|
# Pointer cast: allocate as pointer to struct
|
||||||
|
ptr_type = ir.PointerType(struct_info.ir_type)
|
||||||
|
var = builder.alloca(ptr_type, name=var_name)
|
||||||
|
var.align = 8
|
||||||
|
local_sym_tab[var_name] = LocalSymbol(var, ptr_type, call_type)
|
||||||
|
logger.info(
|
||||||
|
f"Pre-allocated {var_name} for struct pointer cast to {call_type}"
|
||||||
|
)
|
||||||
|
|
||||||
|
elif VmlinuxHandlerRegistry.is_vmlinux_struct(call_type):
|
||||||
|
# When calling struct_name(pointer), we're doing a cast, not construction
|
||||||
|
# So we allocate as a pointer (i64) not as the actual struct
|
||||||
|
var = builder.alloca(ir.IntType(64), name=var_name)
|
||||||
|
var.align = 8
|
||||||
|
local_sym_tab[var_name] = LocalSymbol(
|
||||||
|
var, ir.IntType(64), VmlinuxHandlerRegistry.get_struct_type(call_type)
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"Pre-allocated {var_name} for vmlinux struct pointer cast to {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, rval, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.warning(f"Unsupported call function type for {var_name}")
|
||||||
|
|
||||||
|
|
||||||
|
def _allocate_for_map_method(
|
||||||
|
builder, var_name, rval, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||||
|
):
|
||||||
|
"""Allocate memory for variable assigned from map method (double alloc)."""
|
||||||
|
|
||||||
|
map_name = rval.func.value.id
|
||||||
|
method_name = rval.func.attr
|
||||||
|
|
||||||
|
# NOTE: We will have to special case HashMap.lookup which returns a pointer to value type
|
||||||
|
# The value type can be a struct as well, so we need to handle that properly
|
||||||
|
# This special casing is not ideal, as over time other map methods may need similar handling
|
||||||
|
# But for now, we will just handle lookup specifically
|
||||||
|
if map_name not in map_sym_tab:
|
||||||
|
logger.error(f"Map '{map_name}' not found for allocation")
|
||||||
|
return
|
||||||
|
|
||||||
|
if method_name != "lookup":
|
||||||
|
# Fallback allocation for other map methods
|
||||||
|
_allocate_for_map_method_fallback(builder, var_name, local_sym_tab)
|
||||||
|
return
|
||||||
|
|
||||||
|
map_params = map_sym_tab[map_name].params
|
||||||
|
if map_params["type"] != BPFMapType.HASH:
|
||||||
|
logger.warning(
|
||||||
|
"Map method lookup used on non-hash map, using fallback allocation"
|
||||||
|
)
|
||||||
|
_allocate_for_map_method_fallback(builder, var_name, local_sym_tab)
|
||||||
|
return
|
||||||
|
|
||||||
|
value_type = map_params["value"]
|
||||||
|
# Determine IR type for value
|
||||||
|
if isinstance(value_type, str) and value_type in structs_sym_tab:
|
||||||
|
struct_info = structs_sym_tab[value_type]
|
||||||
|
value_ir_type = struct_info.ir_type
|
||||||
|
else:
|
||||||
|
value_ir_type = ctypes_to_ir(value_type)
|
||||||
|
|
||||||
|
if value_ir_type is None:
|
||||||
|
logger.warning(
|
||||||
|
f"Could not determine IR type for map value '{value_type}', using fallback allocation"
|
||||||
|
)
|
||||||
|
_allocate_for_map_method_fallback(builder, var_name, local_sym_tab)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 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, value_type)
|
||||||
|
# Temporary variable for computed values
|
||||||
|
tmp_ir_type = value_ir_type
|
||||||
|
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 lookup of type {value_ir_type}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _allocate_for_map_method_fallback(builder, var_name, local_sym_tab):
|
||||||
|
"""Fallback allocation for map method variable (i64* and i64**)."""
|
||||||
|
|
||||||
|
# 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 (fallback)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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 _get_type_name(ir_type):
|
||||||
|
"""Get a string representation of an IR type."""
|
||||||
|
if isinstance(ir_type, ir.IntType):
|
||||||
|
return f"i{ir_type.width}"
|
||||||
|
elif isinstance(ir_type, ir.PointerType):
|
||||||
|
return "ptr"
|
||||||
|
elif isinstance(ir_type, ir.ArrayType):
|
||||||
|
return f"[{ir_type.count}x{_get_type_name(ir_type.element)}]"
|
||||||
|
else:
|
||||||
|
return str(ir_type).replace(" ", "")
|
||||||
|
|
||||||
|
|
||||||
|
def allocate_temp_pool(builder, max_temps, local_sym_tab):
|
||||||
|
"""Allocate the temporary scratch space pool for helper arguments."""
|
||||||
|
if not max_temps:
|
||||||
|
logger.info("No temp pool allocation needed")
|
||||||
|
return
|
||||||
|
|
||||||
|
for tmp_type, cnt in max_temps.items():
|
||||||
|
type_name = _get_type_name(tmp_type)
|
||||||
|
logger.info(f"Allocating temp pool of {cnt} variables of type {type_name}")
|
||||||
|
for i in range(cnt):
|
||||||
|
temp_name = f"__helper_temp_{type_name}_{i}"
|
||||||
|
temp_var = builder.alloca(tmp_type, name=temp_name)
|
||||||
|
temp_var.align = _get_alignment(tmp_type)
|
||||||
|
local_sym_tab[temp_name] = LocalSymbol(temp_var, tmp_type)
|
||||||
|
logger.debug(f"Allocated temp variable: {temp_name}")
|
||||||
|
|
||||||
|
|
||||||
|
def _allocate_for_name(builder, var_name, rval, local_sym_tab):
|
||||||
|
"""Allocate memory for variable-to-variable assignment (b = a)."""
|
||||||
|
source_var = rval.id
|
||||||
|
|
||||||
|
if source_var not in local_sym_tab:
|
||||||
|
logger.error(f"Source variable '{source_var}' not found in symbol table")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get type and metadata from source variable
|
||||||
|
source_symbol = local_sym_tab[source_var]
|
||||||
|
|
||||||
|
# Allocate with same type and alignment
|
||||||
|
var = _allocate_with_type(builder, var_name, source_symbol.ir_type)
|
||||||
|
local_sym_tab[var_name] = LocalSymbol(
|
||||||
|
var, source_symbol.ir_type, source_symbol.metadata
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Pre-allocated {var_name} from {source_var} with type {source_symbol.ir_type}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _allocate_for_attribute(builder, var_name, rval, local_sym_tab, structs_sym_tab):
|
||||||
|
"""Allocate memory for struct field-to-variable assignment (a = dat.fld)."""
|
||||||
|
if not isinstance(rval.value, ast.Name):
|
||||||
|
logger.warning(f"Complex attribute access not supported for {var_name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
struct_var = rval.value.id
|
||||||
|
field_name = rval.attr
|
||||||
|
|
||||||
|
# Validate struct and field
|
||||||
|
if struct_var not in local_sym_tab:
|
||||||
|
logger.error(f"Struct variable '{struct_var}' not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
struct_type: type = local_sym_tab[struct_var].metadata
|
||||||
|
if not struct_type or struct_type not in structs_sym_tab:
|
||||||
|
if VmlinuxHandlerRegistry.is_vmlinux_struct(struct_type.__name__):
|
||||||
|
# Handle vmlinux struct field access
|
||||||
|
vmlinux_struct_name = struct_type.__name__
|
||||||
|
if not VmlinuxHandlerRegistry.has_field(vmlinux_struct_name, field_name):
|
||||||
|
logger.error(
|
||||||
|
f"Field '{field_name}' not found in vmlinux struct '{vmlinux_struct_name}'"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
field_type: tuple[ir.GlobalVariable, Field] = (
|
||||||
|
VmlinuxHandlerRegistry.get_field_type(vmlinux_struct_name, field_name)
|
||||||
|
)
|
||||||
|
field_ir, field = field_type
|
||||||
|
|
||||||
|
# Determine the actual IR type based on the field's type
|
||||||
|
actual_ir_type = None
|
||||||
|
|
||||||
|
# Check if it's a ctypes primitive
|
||||||
|
if field.type.__module__ == ctypes.__name__:
|
||||||
|
try:
|
||||||
|
field_size_bytes = ctypes.sizeof(field.type)
|
||||||
|
field_size_bits = field_size_bytes * 8
|
||||||
|
|
||||||
|
if field_size_bits in [8, 16, 32, 64]:
|
||||||
|
# Special case: struct_xdp_md i32 fields should allocate as i64
|
||||||
|
# because load_ctx_field will zero-extend them to i64
|
||||||
|
if (
|
||||||
|
vmlinux_struct_name == "struct_xdp_md"
|
||||||
|
and field_size_bits == 32
|
||||||
|
):
|
||||||
|
actual_ir_type = ir.IntType(64)
|
||||||
|
logger.info(
|
||||||
|
f"Allocating {var_name} as i64 for i32 field from struct_xdp_md.{field_name} "
|
||||||
|
"(will be zero-extended during load)"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
actual_ir_type = ir.IntType(field_size_bits)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"Unusual field size {field_size_bits} bits for {field_name}"
|
||||||
|
)
|
||||||
|
actual_ir_type = ir.IntType(64)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Could not determine size for ctypes field {field_name}: {e}"
|
||||||
|
)
|
||||||
|
actual_ir_type = ir.IntType(64)
|
||||||
|
field_size_bits = 64
|
||||||
|
|
||||||
|
# Check if it's a nested vmlinux struct or complex type
|
||||||
|
elif field.type.__module__ == "vmlinux":
|
||||||
|
# For pointers to structs, use pointer type (64-bit)
|
||||||
|
if field.ctype_complex_type is not None and issubclass(
|
||||||
|
field.ctype_complex_type, ctypes._Pointer
|
||||||
|
):
|
||||||
|
actual_ir_type = ir.IntType(64) # Pointer is always 64-bit
|
||||||
|
field_size_bits = 64
|
||||||
|
# For embedded structs, this is more complex - might need different handling
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"Field {field_name} is a nested vmlinux struct, using i64 for now"
|
||||||
|
)
|
||||||
|
actual_ir_type = ir.IntType(64)
|
||||||
|
field_size_bits = 64
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"Unknown field type module {field.type.__module__} for {field_name}"
|
||||||
|
)
|
||||||
|
actual_ir_type = ir.IntType(64)
|
||||||
|
field_size_bits = 64
|
||||||
|
|
||||||
|
# Pre-allocate the tmp storage used by load_struct_field (so we don't alloca inside handler)
|
||||||
|
tmp_name = f"{struct_var}_{field_name}_tmp"
|
||||||
|
tmp_ir_type = ir.IntType(field_size_bits)
|
||||||
|
tmp_var = builder.alloca(tmp_ir_type, name=tmp_name)
|
||||||
|
tmp_var.align = tmp_ir_type.width // 8
|
||||||
|
local_sym_tab[tmp_name] = LocalSymbol(tmp_var, tmp_ir_type)
|
||||||
|
logger.info(
|
||||||
|
f"Pre-allocated temp {tmp_name} (i{field_size_bits}) for vmlinux field read {vmlinux_struct_name}.{field_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Allocate with the actual IR type for the destination var
|
||||||
|
var = _allocate_with_type(builder, var_name, actual_ir_type)
|
||||||
|
local_sym_tab[var_name] = LocalSymbol(var, actual_ir_type, field)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Pre-allocated {var_name} as {actual_ir_type} from vmlinux struct {vmlinux_struct_name}.{field_name}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
logger.error(f"Struct type '{struct_type}' not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
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 type
|
||||||
|
field_type = struct_info.field_type(field_name)
|
||||||
|
|
||||||
|
# Special case: char array -> allocate as i8* pointer instead
|
||||||
|
if (
|
||||||
|
isinstance(field_type, ir.ArrayType)
|
||||||
|
and isinstance(field_type.element, ir.IntType)
|
||||||
|
and field_type.element.width == 8
|
||||||
|
):
|
||||||
|
alloc_type = ir.PointerType(ir.IntType(8))
|
||||||
|
logger.info(f"Allocating {var_name} as i8* (pointer to char array)")
|
||||||
|
else:
|
||||||
|
alloc_type = field_type
|
||||||
|
|
||||||
|
var = _allocate_with_type(builder, var_name, alloc_type)
|
||||||
|
local_sym_tab[var_name] = LocalSymbol(var, alloc_type)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Pre-allocated {var_name} from {struct_var}.{field_name} with type {alloc_type}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _allocate_with_type(builder, var_name, ir_type):
|
||||||
|
"""Allocate variable with appropriate alignment for type."""
|
||||||
|
var = builder.alloca(ir_type, name=var_name)
|
||||||
|
var.align = _get_alignment(ir_type)
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
def _get_alignment(ir_type):
|
||||||
|
"""Get appropriate alignment for IR type."""
|
||||||
|
if isinstance(ir_type, ir.IntType):
|
||||||
|
return ir_type.width // 8
|
||||||
|
elif isinstance(ir_type, ir.ArrayType) and isinstance(ir_type.element, ir.IntType):
|
||||||
|
return ir_type.element.width // 8
|
||||||
|
else:
|
||||||
|
return 8 # Default: pointer size
|
||||||
293
pythonbpf/assign_pass.py
Normal file
293
pythonbpf/assign_pass.py
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
import ast
|
||||||
|
import logging
|
||||||
|
from inspect import isclass
|
||||||
|
|
||||||
|
from llvmlite import ir
|
||||||
|
from pythonbpf.expr import eval_expr
|
||||||
|
from pythonbpf.helper import emit_probe_read_kernel_str_call
|
||||||
|
from pythonbpf.type_deducer import ctypes_to_ir
|
||||||
|
from pythonbpf.vmlinux_parser.dependency_node import Field
|
||||||
|
|
||||||
|
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)
|
||||||
|
field_type = struct_info.field_type(field_name)
|
||||||
|
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}.{field_name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
val, val_type = val_result
|
||||||
|
|
||||||
|
# Special case: i8* string to [N x i8] char array
|
||||||
|
if _is_char_array(field_type) and _is_i8_ptr(val_type):
|
||||||
|
_copy_string_to_char_array(
|
||||||
|
func,
|
||||||
|
module,
|
||||||
|
builder,
|
||||||
|
val,
|
||||||
|
field_ptr,
|
||||||
|
field_type,
|
||||||
|
local_sym_tab,
|
||||||
|
map_sym_tab,
|
||||||
|
structs_sym_tab,
|
||||||
|
)
|
||||||
|
logger.info(f"Copied string to char array {var_name}.{field_name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Regular assignment
|
||||||
|
builder.store(val, field_ptr)
|
||||||
|
logger.info(f"Assigned to struct field {var_name}.{field_name}")
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_string_to_char_array(
|
||||||
|
func,
|
||||||
|
module,
|
||||||
|
builder,
|
||||||
|
src_ptr,
|
||||||
|
dst_ptr,
|
||||||
|
array_type,
|
||||||
|
local_sym_tab,
|
||||||
|
map_sym_tab,
|
||||||
|
struct_sym_tab,
|
||||||
|
):
|
||||||
|
"""Copy string (i8*) to char array ([N x i8]) using bpf_probe_read_kernel_str"""
|
||||||
|
|
||||||
|
array_size = array_type.count
|
||||||
|
|
||||||
|
# Get pointer to first element: [N x i8]* -> i8*
|
||||||
|
dst_i8_ptr = builder.gep(
|
||||||
|
dst_ptr,
|
||||||
|
[ir.Constant(ir.IntType(32), 0), ir.Constant(ir.IntType(32), 0)],
|
||||||
|
inbounds=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use the shared emitter function
|
||||||
|
emit_probe_read_kernel_str_call(builder, dst_i8_ptr, array_size, src_ptr)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_char_array(ir_type):
|
||||||
|
"""Check if type is [N x i8]."""
|
||||||
|
return (
|
||||||
|
isinstance(ir_type, ir.ArrayType)
|
||||||
|
and isinstance(ir_type.element, ir.IntType)
|
||||||
|
and ir_type.element.width == 8
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_i8_ptr(ir_type):
|
||||||
|
"""Check if type is i8*."""
|
||||||
|
return (
|
||||||
|
isinstance(ir_type, ir.PointerType)
|
||||||
|
and isinstance(ir_type.pointee, ir.IntType)
|
||||||
|
and ir_type.pointee.width == 8
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Special case: struct field char array -> pointer
|
||||||
|
# Handle this before eval_expr to get the pointer, not the value
|
||||||
|
if isinstance(rval, ast.Attribute) and isinstance(rval.value, ast.Name):
|
||||||
|
converted_val = _try_convert_char_array_to_ptr(
|
||||||
|
rval, var_type, builder, local_sym_tab, structs_sym_tab
|
||||||
|
)
|
||||||
|
if converted_val is not None:
|
||||||
|
builder.store(converted_val, var_ptr)
|
||||||
|
logger.info(f"Assigned char array pointer to {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}, expected {var_type}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if val_type != var_type:
|
||||||
|
# Handle vmlinux struct pointers - they're represented as Python classes but are i64 pointers
|
||||||
|
if isclass(val_type) and (val_type.__module__ == "vmlinux"):
|
||||||
|
logger.info("Handling vmlinux struct pointer assignment")
|
||||||
|
# vmlinux struct pointers: val is a pointer, need to convert to i64
|
||||||
|
if isinstance(var_type, ir.IntType) and var_type.width == 64:
|
||||||
|
# Convert pointer to i64 using ptrtoint
|
||||||
|
if isinstance(val.type, ir.PointerType):
|
||||||
|
val = builder.ptrtoint(val, ir.IntType(64))
|
||||||
|
logger.info(
|
||||||
|
"Converted vmlinux struct pointer to i64 using ptrtoint"
|
||||||
|
)
|
||||||
|
builder.store(val, var_ptr)
|
||||||
|
logger.info(f"Assigned vmlinux struct pointer to {var_name} (i64)")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
f"Type mismatch: vmlinux struct pointer requires i64, got {var_type}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
# Handle user-defined struct pointer casts
|
||||||
|
# val_type is a string (struct name), var_type is a pointer to the struct
|
||||||
|
if isinstance(val_type, str) and val_type in structs_sym_tab:
|
||||||
|
struct_info = structs_sym_tab[val_type]
|
||||||
|
expected_ptr_type = ir.PointerType(struct_info.ir_type)
|
||||||
|
|
||||||
|
# Check if var_type matches the expected pointer type
|
||||||
|
if isinstance(var_type, ir.PointerType) and var_type == expected_ptr_type:
|
||||||
|
# val is already the correct pointer type from inttoptr/bitcast
|
||||||
|
builder.store(val, var_ptr)
|
||||||
|
logger.info(f"Assigned user-defined struct pointer cast to {var_name}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
f"Type mismatch: user-defined struct pointer cast requires pointer type, got {var_type}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
if isinstance(val_type, Field):
|
||||||
|
logger.info("Handling assignment to struct field")
|
||||||
|
# Special handling for struct_xdp_md i32 fields that are zero-extended to i64
|
||||||
|
# The load_ctx_field already extended them, so val is i64 but val_type.type shows c_uint
|
||||||
|
if (
|
||||||
|
hasattr(val_type, "type")
|
||||||
|
and val_type.type.__name__ == "c_uint"
|
||||||
|
and isinstance(var_type, ir.IntType)
|
||||||
|
and var_type.width == 64
|
||||||
|
):
|
||||||
|
# This is the struct_xdp_md case - value is already i64
|
||||||
|
builder.store(val, var_ptr)
|
||||||
|
logger.info(
|
||||||
|
f"Assigned zero-extended struct_xdp_md i32 field to {var_name} (i64)"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
# TODO: handling only ctype struct fields for now. Handle other stuff too later.
|
||||||
|
elif var_type == ctypes_to_ir(val_type.type.__name__):
|
||||||
|
builder.store(val, var_ptr)
|
||||||
|
logger.info(f"Assigned ctype struct field to {var_name}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
f"Failed to assign ctype struct field to {var_name}: {val_type} != {var_type}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
elif 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
|
||||||
|
|
||||||
|
|
||||||
|
def _try_convert_char_array_to_ptr(
|
||||||
|
rval, var_type, builder, local_sym_tab, structs_sym_tab
|
||||||
|
):
|
||||||
|
"""Try to convert char array field to i8* pointer"""
|
||||||
|
# Only convert if target is i8*
|
||||||
|
if not (
|
||||||
|
isinstance(var_type, ir.PointerType)
|
||||||
|
and isinstance(var_type.pointee, ir.IntType)
|
||||||
|
and var_type.pointee.width == 8
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
struct_var = rval.value.id
|
||||||
|
field_name = rval.attr
|
||||||
|
|
||||||
|
# Validate struct
|
||||||
|
if struct_var not in local_sym_tab:
|
||||||
|
return None
|
||||||
|
|
||||||
|
struct_type = local_sym_tab[struct_var].metadata
|
||||||
|
if not struct_type or struct_type not in structs_sym_tab:
|
||||||
|
return None
|
||||||
|
|
||||||
|
struct_info = structs_sym_tab[struct_type]
|
||||||
|
if field_name not in struct_info.fields:
|
||||||
|
return None
|
||||||
|
|
||||||
|
field_type = struct_info.field_type(field_name)
|
||||||
|
|
||||||
|
# Check if it's a char array
|
||||||
|
if not (
|
||||||
|
isinstance(field_type, ir.ArrayType)
|
||||||
|
and isinstance(field_type.element, ir.IntType)
|
||||||
|
and field_type.element.width == 8
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get pointer to struct field
|
||||||
|
struct_ptr = local_sym_tab[struct_var].var
|
||||||
|
field_ptr = struct_info.gep(builder, struct_ptr, field_name)
|
||||||
|
|
||||||
|
# GEP to first element: [N x i8]* -> i8*
|
||||||
|
return builder.gep(
|
||||||
|
field_ptr,
|
||||||
|
[ir.Constant(ir.IntType(32), 0), ir.Constant(ir.IntType(32), 0)],
|
||||||
|
inbounds=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,498 +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, struct_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, struct_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.")
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
f"Variable {value.value.id} not found in local symbol table.")
|
|
||||||
elif isinstance(value.value, ast.Attribute):
|
|
||||||
# object field access from struct
|
|
||||||
if isinstance(value.value.value, ast.Name) and local_sym_tab and value.value.value.id in local_sym_tab:
|
|
||||||
var_name = value.value.value.id
|
|
||||||
field_name = value.value.attr
|
|
||||||
if local_var_metadata and var_name in local_var_metadata:
|
|
||||||
var_type = local_var_metadata[var_name]
|
|
||||||
if var_type in struct_sym_tab:
|
|
||||||
struct_info = struct_sym_tab[var_type]
|
|
||||||
if field_name in struct_info["fields"]:
|
|
||||||
field_index = struct_info["fields"][field_name]
|
|
||||||
field_type = struct_info["field_types"][field_index]
|
|
||||||
if isinstance(field_type, ir.IntType):
|
|
||||||
fmt_parts.append("%lld")
|
|
||||||
exprs.append(value.value)
|
|
||||||
elif field_type == ir.PointerType(ir.IntType(8)):
|
|
||||||
fmt_parts.append("%s")
|
|
||||||
exprs.append(value.value)
|
|
||||||
else:
|
|
||||||
raise NotImplementedError(
|
|
||||||
"Only integer and pointer types are supported in formatted values.")
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
f"Field {field_name} not found in struct {var_type}.")
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
f"Struct type {var_type} for variable {var_name} not found in struct symbol table.")
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
f"Metadata for variable {var_name} not found in local variable metadata.")
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
f"Variable {value.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( # 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())
|
|
||||||
|
|
||||||
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]:
|
|
||||||
print(f"{ast.dump(expr)}")
|
|
||||||
val, _ = eval_expr(func, module, builder,
|
|
||||||
expr, local_sym_tab, None, struct_sym_tab, local_var_metadata)
|
|
||||||
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, struct_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):
|
|
||||||
print(local_var_metadata)
|
|
||||||
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, struct_sym_tab, local_var_metadata)
|
|
||||||
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,16 +1,58 @@
|
|||||||
import ast
|
import ast
|
||||||
from llvmlite import ir
|
from llvmlite import ir
|
||||||
from .license_pass import license_processing
|
from .license_pass import license_processing
|
||||||
from .functions_pass import func_proc
|
from .functions import func_proc
|
||||||
from .maps_pass import maps_proc
|
from .maps import maps_proc
|
||||||
from .structs_pass import structs_proc
|
from .structs import structs_proc
|
||||||
from .globals_pass import globals_processing
|
from .vmlinux_parser import vmlinux_proc
|
||||||
|
from pythonbpf.vmlinux_parser.vmlinux_exports_handler import VmlinuxHandler
|
||||||
|
from .expr import VmlinuxHandlerRegistry
|
||||||
|
from .globals_pass import (
|
||||||
|
globals_list_creation,
|
||||||
|
globals_processing,
|
||||||
|
populate_global_symbol_table,
|
||||||
|
)
|
||||||
|
from .debuginfo import DW_LANG_C11, DwarfBehaviorEnum, DebugInfoGenerator
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import inspect
|
import inspect
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from pylibbpf import BpfProgram
|
from pylibbpf import BpfObject
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from logging import Logger
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
logger: Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
VERSION = "v0.1.8"
|
||||||
|
|
||||||
|
|
||||||
|
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 bpf_passthrough_gen(module):
|
||||||
|
i32_ty = ir.IntType(32)
|
||||||
|
ptr_ty = ir.PointerType(ir.IntType(8))
|
||||||
|
fnty = ir.FunctionType(ptr_ty, [i32_ty, ptr_ty])
|
||||||
|
|
||||||
|
# Declare the intrinsic
|
||||||
|
passthrough = ir.Function(module, fnty, "llvm.bpf.passthrough.p0.p0")
|
||||||
|
|
||||||
|
# Set function attributes
|
||||||
|
# TODO: the ones commented are supposed to be there but cannot be added due to llvmlite limitations at the moment
|
||||||
|
# passthrough.attributes.add("nofree")
|
||||||
|
# passthrough.attributes.add("nosync")
|
||||||
|
passthrough.attributes.add("nounwind")
|
||||||
|
# passthrough.attributes.add("memory(none)")
|
||||||
|
|
||||||
|
return passthrough
|
||||||
|
|
||||||
|
|
||||||
def find_bpf_chunks(tree):
|
def find_bpf_chunks(tree):
|
||||||
@ -27,21 +69,34 @@ def find_bpf_chunks(tree):
|
|||||||
|
|
||||||
def processor(source_code, filename, module):
|
def processor(source_code, filename, module):
|
||||||
tree = ast.parse(source_code, filename)
|
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)
|
bpf_chunks = find_bpf_chunks(tree)
|
||||||
for func_node in bpf_chunks:
|
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}")
|
||||||
|
|
||||||
structs_sym_tab = structs_proc(tree, module, bpf_chunks)
|
bpf_passthrough_gen(module)
|
||||||
map_sym_tab = maps_proc(tree, module, bpf_chunks)
|
|
||||||
func_proc(tree, module, bpf_chunks, map_sym_tab, structs_sym_tab)
|
|
||||||
|
|
||||||
|
vmlinux_symtab = vmlinux_proc(tree, module)
|
||||||
|
if vmlinux_symtab:
|
||||||
|
handler = VmlinuxHandler.initialize(vmlinux_symtab)
|
||||||
|
VmlinuxHandlerRegistry.set_handler(handler)
|
||||||
|
|
||||||
|
populate_global_symbol_table(tree, module)
|
||||||
license_processing(tree, module)
|
license_processing(tree, module)
|
||||||
globals_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, structs_sym_tab)
|
||||||
|
func_proc(tree, module, bpf_chunks, map_sym_tab, structs_sym_tab)
|
||||||
|
|
||||||
|
globals_list_creation(tree, module)
|
||||||
|
return structs_sym_tab, map_sym_tab
|
||||||
|
|
||||||
|
|
||||||
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:
|
with open(filename) as f:
|
||||||
source = f.read()
|
source = f.read()
|
||||||
|
|
||||||
@ -49,61 +104,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.data_layout = "e-m:e-p:64:64-i64:64-i128:128-n32:64-S128"
|
||||||
module.triple = "bpf"
|
module.triple = "bpf"
|
||||||
|
|
||||||
if not hasattr(module, '_debug_compile_unit'):
|
if not hasattr(module, "_debug_compile_unit"):
|
||||||
module._file_metadata = module.add_debug_info("DIFile", { # type: ignore
|
debug_generator = DebugInfoGenerator(module)
|
||||||
"filename": filename,
|
debug_generator.generate_file_metadata(filename, os.path.dirname(filename))
|
||||||
"directory": 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,
|
||||||
|
)
|
||||||
|
|
||||||
module._debug_compile_unit = module.add_debug_info("DICompileUnit", { # type: ignore
|
structs_sym_tab, maps_sym_tab = processor(source, filename, module)
|
||||||
"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(
|
wchar_size = module.add_metadata(
|
||||||
"llvm.dbg.cu", module._debug_compile_unit) # type: ignore
|
[
|
||||||
|
DwarfBehaviorEnum.ERROR_IF_MISMATCH,
|
||||||
processor(source, filename, module)
|
"wchar_size",
|
||||||
|
ir.Constant(ir.IntType(32), 4),
|
||||||
wchar_size = module.add_metadata([ir.Constant(ir.IntType(32), 1),
|
]
|
||||||
"wchar_size",
|
)
|
||||||
ir.Constant(ir.IntType(32), 4)])
|
frame_pointer = module.add_metadata(
|
||||||
frame_pointer = module.add_metadata([ir.Constant(ir.IntType(32), 7),
|
[
|
||||||
"frame-pointer",
|
DwarfBehaviorEnum.OVERRIDE_USE_LARGEST,
|
||||||
ir.Constant(ir.IntType(32), 2)])
|
"frame-pointer",
|
||||||
|
ir.Constant(ir.IntType(32), 2),
|
||||||
|
]
|
||||||
|
)
|
||||||
# Add Debug Info Version (3 = DWARF v3, which LLVM expects)
|
# 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 = module.add_metadata(
|
||||||
"Debug Info Version",
|
[
|
||||||
ir.Constant(ir.IntType(32), 3)])
|
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)
|
# Add explicit DWARF version 5
|
||||||
dwarf_version = module.add_metadata([ir.Constant(ir.IntType(32), 2),
|
dwarf_version = module.add_metadata(
|
||||||
"Dwarf Version",
|
[
|
||||||
ir.Constant(ir.IntType(32), 4)])
|
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", wchar_size)
|
||||||
module.add_named_metadata("llvm.module.flags", frame_pointer)
|
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", debug_info_version)
|
||||||
module.add_named_metadata("llvm.module.flags", dwarf_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: str = finalize_module(str(module))
|
||||||
|
|
||||||
|
logger.info(f"IR written to {output}")
|
||||||
with open(output, "w") as f:
|
with open(output, "w") as f:
|
||||||
f.write(f"source_filename = \"{filename}\"\n")
|
f.write(f'source_filename = "{filename}"\n')
|
||||||
f.write(str(module))
|
f.write(module_string)
|
||||||
f.write("\n")
|
f.write("\n")
|
||||||
|
|
||||||
return output
|
return output, structs_sym_tab, maps_sym_tab
|
||||||
|
|
||||||
|
|
||||||
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.WARNING) -> bool:
|
||||||
# Look one level up the stack to the caller of this function
|
# Look one level up the stack to the caller of this function
|
||||||
caller_frame = inspect.stack()[1]
|
caller_frame = inspect.stack()[1]
|
||||||
caller_file = Path(caller_frame.filename).resolve()
|
caller_file = Path(caller_frame.filename).resolve()
|
||||||
@ -111,29 +203,32 @@ def compile():
|
|||||||
ll_file = Path("/tmp") / caller_file.with_suffix(".ll").name
|
ll_file = Path("/tmp") / caller_file.with_suffix(".ll").name
|
||||||
o_file = caller_file.with_suffix(".o")
|
o_file = caller_file.with_suffix(".o")
|
||||||
|
|
||||||
compile_to_ir(str(caller_file), str(ll_file))
|
_, structs_sym_tab, maps_sym_tab = compile_to_ir(
|
||||||
|
str(caller_file), str(ll_file), loglevel=loglevel
|
||||||
|
)
|
||||||
|
|
||||||
subprocess.run([
|
if not _run_llc(ll_file, o_file):
|
||||||
"llc", "-march=bpf", "-filetype=obj", "-O2",
|
logger.error("Compilation to object file failed.")
|
||||||
str(ll_file), "-o", str(o_file)
|
return False
|
||||||
], check=True)
|
|
||||||
|
|
||||||
print(f"Object written to {o_file}, {ll_file} can be removed")
|
logger.info(f"Object written to {o_file}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def BPF() -> BpfProgram:
|
def BPF(loglevel=logging.WARNING) -> BpfObject:
|
||||||
caller_frame = inspect.stack()[1]
|
caller_frame = inspect.stack()[1]
|
||||||
src = inspect.getsource(caller_frame.frame)
|
src = inspect.getsource(caller_frame.frame)
|
||||||
with tempfile.NamedTemporaryFile(mode="w+", delete=True, suffix=".py") as f, \
|
with (
|
||||||
tempfile.NamedTemporaryFile(mode="w+", delete=True, suffix=".ll") as inter, \
|
tempfile.NamedTemporaryFile(mode="w+", delete=True, suffix=".py") as f,
|
||||||
tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix=".o") as obj_file:
|
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.write(src)
|
||||||
f.flush()
|
f.flush()
|
||||||
source = f.name
|
source = f.name
|
||||||
compile_to_ir(source, str(inter.name))
|
_, structs_sym_tab, maps_sym_tab = compile_to_ir(
|
||||||
subprocess.run([
|
source, str(inter.name), loglevel=loglevel
|
||||||
"llc", "-march=bpf", "-filetype=obj", "-O2",
|
)
|
||||||
str(inter.name), "-o", str(obj_file.name)
|
_run_llc(str(inter.name), str(obj_file.name))
|
||||||
], check=True)
|
|
||||||
|
|
||||||
return BpfProgram(str(obj_file.name))
|
return BpfObject(str(obj_file.name), structs=structs_sym_tab)
|
||||||
|
|||||||
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"]
|
||||||
270
pythonbpf/debuginfo/debug_info_generator.py
Normal file
270
pythonbpf/debuginfo/debug_info_generator.py
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
"""
|
||||||
|
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_uint8_type(self) -> Any:
|
||||||
|
"""Get debug info for signed 8-bit integer"""
|
||||||
|
return self.get_basic_type("char", 8, dc.DW_ATE_unsigned)
|
||||||
|
|
||||||
|
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],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_array_type_vmlinux(self, type_info: Any, count: int) -> Any:
|
||||||
|
"""Create an array type of the given base type with specified count"""
|
||||||
|
base_type, type_sizing = type_info
|
||||||
|
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": type_sizing,
|
||||||
|
"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_member_vmlinux(
|
||||||
|
self, name: str, base_type_with_size: Any, offset: int
|
||||||
|
) -> Any:
|
||||||
|
"""Create a struct member with the given name, type, and offset"""
|
||||||
|
base_type, type_size = base_type_with_size
|
||||||
|
return self.module.add_debug_info(
|
||||||
|
"DIDerivedType",
|
||||||
|
{
|
||||||
|
"tag": dc.DW_TAG_member,
|
||||||
|
"name": name,
|
||||||
|
"file": self.module._file_metadata,
|
||||||
|
"baseType": base_type,
|
||||||
|
"size": type_size,
|
||||||
|
"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_struct_type_with_name(
|
||||||
|
self, name: str, 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",
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"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", {})},
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_int64_type(self):
|
||||||
|
return self.get_basic_type("long", 64, dc.DW_ATE_signed)
|
||||||
|
|
||||||
|
def create_subroutine_type(self, return_type, param_types):
|
||||||
|
"""
|
||||||
|
Create a DISubroutineType given return type and list of parameter types.
|
||||||
|
Equivalent to: !DISubroutineType(types: !{ret, args...})
|
||||||
|
"""
|
||||||
|
type_array = [return_type]
|
||||||
|
if isinstance(param_types, (list, tuple)):
|
||||||
|
type_array.extend(param_types)
|
||||||
|
else:
|
||||||
|
type_array.append(param_types)
|
||||||
|
return self.module.add_debug_info("DISubroutineType", {"types": type_array})
|
||||||
|
|
||||||
|
def create_local_variable_debug_info(
|
||||||
|
self, name: str, arg: int, var_type: Any
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Create debug info for a local variable (DILocalVariable) without scope.
|
||||||
|
Example:
|
||||||
|
!DILocalVariable(name: "ctx", arg: 1, file: !3, line: 20, type: !7)
|
||||||
|
"""
|
||||||
|
return self.module.add_debug_info(
|
||||||
|
"DILocalVariable",
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"arg": arg,
|
||||||
|
"file": self.module._file_metadata,
|
||||||
|
"type": var_type,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_scope_to_local_variable(self, local_variable_debug_info, scope_value):
|
||||||
|
"""
|
||||||
|
Add scope information to an existing local variable debug info object.
|
||||||
|
"""
|
||||||
|
# TODO: this is a workaround a flaw in the debug info generation. Fix this if possible in the future.
|
||||||
|
# We should not be touching llvmlite's internals like this.
|
||||||
|
if hasattr(local_variable_debug_info, "operands"):
|
||||||
|
# LLVM metadata operands is a tuple, so we need to rebuild it
|
||||||
|
existing_operands = local_variable_debug_info.operands
|
||||||
|
|
||||||
|
# Convert tuple to list, add scope, convert back to tuple
|
||||||
|
operands_list = list(existing_operands)
|
||||||
|
operands_list.append(("scope", scope_value))
|
||||||
|
|
||||||
|
# Reassign the new tuple
|
||||||
|
local_variable_debug_info.operands = tuple(operands_list)
|
||||||
|
|
||||||
|
def create_subprogram(
|
||||||
|
self, name: str, subroutine_type: Any, retained_nodes: List[Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Create a DISubprogram for a function.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Function name
|
||||||
|
subroutine_type: DISubroutineType for the function signature
|
||||||
|
retained_nodes: List of DILocalVariable nodes for function parameters/variables
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DISubprogram metadata
|
||||||
|
"""
|
||||||
|
return self.module.add_debug_info(
|
||||||
|
"DISubprogram",
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"scope": self.module._file_metadata,
|
||||||
|
"file": self.module._file_metadata,
|
||||||
|
"type": subroutine_type,
|
||||||
|
# TODO: the following flags do not exist at the moment in our dwarf constants file. We need to add them.
|
||||||
|
# "flags": dc.DW_FLAG_Prototyped | dc.DW_FLAG_AllCallsDescribed,
|
||||||
|
# "spFlags": dc.DW_SPFLAG_Definition | dc.DW_SPFLAG_Optimized,
|
||||||
|
"unit": self.module._debug_compile_unit,
|
||||||
|
"retainedNodes": retained_nodes,
|
||||||
|
},
|
||||||
|
is_distinct=True,
|
||||||
|
)
|
||||||
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_compile = 0x05
|
||||||
DW_UT_split_type = 0x06
|
DW_UT_split_type = 0x06
|
||||||
DW_UT_lo_user = 0x80
|
DW_UT_lo_user = 0x80
|
||||||
DW_UT_hi_user = 0xff
|
DW_UT_hi_user = 0xFF
|
||||||
|
|
||||||
DW_TAG_array_type = 0x01
|
DW_TAG_array_type = 0x01
|
||||||
DW_TAG_class_type = 0x02
|
DW_TAG_class_type = 0x02
|
||||||
@ -15,10 +15,10 @@ DW_TAG_entry_point = 0x03
|
|||||||
DW_TAG_enumeration_type = 0x04
|
DW_TAG_enumeration_type = 0x04
|
||||||
DW_TAG_formal_parameter = 0x05
|
DW_TAG_formal_parameter = 0x05
|
||||||
DW_TAG_imported_declaration = 0x08
|
DW_TAG_imported_declaration = 0x08
|
||||||
DW_TAG_label = 0x0a
|
DW_TAG_label = 0x0A
|
||||||
DW_TAG_lexical_block = 0x0b
|
DW_TAG_lexical_block = 0x0B
|
||||||
DW_TAG_member = 0x0d
|
DW_TAG_member = 0x0D
|
||||||
DW_TAG_pointer_type = 0x0f
|
DW_TAG_pointer_type = 0x0F
|
||||||
DW_TAG_reference_type = 0x10
|
DW_TAG_reference_type = 0x10
|
||||||
DW_TAG_compile_unit = 0x11
|
DW_TAG_compile_unit = 0x11
|
||||||
DW_TAG_string_type = 0x12
|
DW_TAG_string_type = 0x12
|
||||||
@ -28,12 +28,12 @@ DW_TAG_typedef = 0x16
|
|||||||
DW_TAG_union_type = 0x17
|
DW_TAG_union_type = 0x17
|
||||||
DW_TAG_unspecified_parameters = 0x18
|
DW_TAG_unspecified_parameters = 0x18
|
||||||
DW_TAG_variant = 0x19
|
DW_TAG_variant = 0x19
|
||||||
DW_TAG_common_block = 0x1a
|
DW_TAG_common_block = 0x1A
|
||||||
DW_TAG_common_inclusion = 0x1b
|
DW_TAG_common_inclusion = 0x1B
|
||||||
DW_TAG_inheritance = 0x1c
|
DW_TAG_inheritance = 0x1C
|
||||||
DW_TAG_inlined_subroutine = 0x1d
|
DW_TAG_inlined_subroutine = 0x1D
|
||||||
DW_TAG_module = 0x1e
|
DW_TAG_module = 0x1E
|
||||||
DW_TAG_ptr_to_member_type = 0x1f
|
DW_TAG_ptr_to_member_type = 0x1F
|
||||||
DW_TAG_set_type = 0x20
|
DW_TAG_set_type = 0x20
|
||||||
DW_TAG_subrange_type = 0x21
|
DW_TAG_subrange_type = 0x21
|
||||||
DW_TAG_with_stmt = 0x22
|
DW_TAG_with_stmt = 0x22
|
||||||
@ -44,12 +44,12 @@ DW_TAG_const_type = 0x26
|
|||||||
DW_TAG_constant = 0x27
|
DW_TAG_constant = 0x27
|
||||||
DW_TAG_enumerator = 0x28
|
DW_TAG_enumerator = 0x28
|
||||||
DW_TAG_file_type = 0x29
|
DW_TAG_file_type = 0x29
|
||||||
DW_TAG_friend = 0x2a
|
DW_TAG_friend = 0x2A
|
||||||
DW_TAG_namelist = 0x2b
|
DW_TAG_namelist = 0x2B
|
||||||
DW_TAG_namelist_item = 0x2c
|
DW_TAG_namelist_item = 0x2C
|
||||||
DW_TAG_packed_type = 0x2d
|
DW_TAG_packed_type = 0x2D
|
||||||
DW_TAG_subprogram = 0x2e
|
DW_TAG_subprogram = 0x2E
|
||||||
DW_TAG_template_type_parameter = 0x2f
|
DW_TAG_template_type_parameter = 0x2F
|
||||||
DW_TAG_template_value_parameter = 0x30
|
DW_TAG_template_value_parameter = 0x30
|
||||||
DW_TAG_thrown_type = 0x31
|
DW_TAG_thrown_type = 0x31
|
||||||
DW_TAG_try_block = 0x32
|
DW_TAG_try_block = 0x32
|
||||||
@ -60,11 +60,11 @@ DW_TAG_dwarf_procedure = 0x36
|
|||||||
DW_TAG_restrict_type = 0x37
|
DW_TAG_restrict_type = 0x37
|
||||||
DW_TAG_interface_type = 0x38
|
DW_TAG_interface_type = 0x38
|
||||||
DW_TAG_namespace = 0x39
|
DW_TAG_namespace = 0x39
|
||||||
DW_TAG_imported_module = 0x3a
|
DW_TAG_imported_module = 0x3A
|
||||||
DW_TAG_unspecified_type = 0x3b
|
DW_TAG_unspecified_type = 0x3B
|
||||||
DW_TAG_partial_unit = 0x3c
|
DW_TAG_partial_unit = 0x3C
|
||||||
DW_TAG_imported_unit = 0x3d
|
DW_TAG_imported_unit = 0x3D
|
||||||
DW_TAG_condition = 0x3f
|
DW_TAG_condition = 0x3F
|
||||||
DW_TAG_shared_type = 0x40
|
DW_TAG_shared_type = 0x40
|
||||||
DW_TAG_type_unit = 0x41
|
DW_TAG_type_unit = 0x41
|
||||||
DW_TAG_rvalue_reference_type = 0x42
|
DW_TAG_rvalue_reference_type = 0x42
|
||||||
@ -75,8 +75,8 @@ DW_TAG_dynamic_type = 0x46
|
|||||||
DW_TAG_atomic_type = 0x47
|
DW_TAG_atomic_type = 0x47
|
||||||
DW_TAG_call_site = 0x48
|
DW_TAG_call_site = 0x48
|
||||||
DW_TAG_call_site_parameter = 0x49
|
DW_TAG_call_site_parameter = 0x49
|
||||||
DW_TAG_skeleton_unit = 0x4a
|
DW_TAG_skeleton_unit = 0x4A
|
||||||
DW_TAG_immutable_type = 0x4b
|
DW_TAG_immutable_type = 0x4B
|
||||||
DW_TAG_lo_user = 0x4080
|
DW_TAG_lo_user = 0x4080
|
||||||
DW_TAG_MIPS_loop = 0x4081
|
DW_TAG_MIPS_loop = 0x4081
|
||||||
DW_TAG_format_label = 0x4101
|
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_template_parameter_pack = 0x4107
|
||||||
DW_TAG_GNU_formal_parameter_pack = 0x4108
|
DW_TAG_GNU_formal_parameter_pack = 0x4108
|
||||||
DW_TAG_GNU_call_site = 0x4109
|
DW_TAG_GNU_call_site = 0x4109
|
||||||
DW_TAG_GNU_call_site_parameter = 0x410a
|
DW_TAG_GNU_call_site_parameter = 0x410A
|
||||||
DW_TAG_hi_user = 0xffff
|
DW_TAG_hi_user = 0xFFFF
|
||||||
|
|
||||||
DW_CHILDREN_no = 0
|
DW_CHILDREN_no = 0
|
||||||
DW_CHILDREN_yes = 1
|
DW_CHILDREN_yes = 1
|
||||||
@ -98,9 +98,9 @@ DW_AT_sibling = 0x01
|
|||||||
DW_AT_location = 0x02
|
DW_AT_location = 0x02
|
||||||
DW_AT_name = 0x03
|
DW_AT_name = 0x03
|
||||||
DW_AT_ordering = 0x09
|
DW_AT_ordering = 0x09
|
||||||
DW_AT_byte_size = 0x0b
|
DW_AT_byte_size = 0x0B
|
||||||
DW_AT_bit_offset = 0x0c
|
DW_AT_bit_offset = 0x0C
|
||||||
DW_AT_bit_size = 0x0d
|
DW_AT_bit_size = 0x0D
|
||||||
DW_AT_stmt_list = 0x10
|
DW_AT_stmt_list = 0x10
|
||||||
DW_AT_low_pc = 0x11
|
DW_AT_low_pc = 0x11
|
||||||
DW_AT_high_pc = 0x12
|
DW_AT_high_pc = 0x12
|
||||||
@ -110,20 +110,20 @@ DW_AT_discr_value = 0x16
|
|||||||
DW_AT_visibility = 0x17
|
DW_AT_visibility = 0x17
|
||||||
DW_AT_import = 0x18
|
DW_AT_import = 0x18
|
||||||
DW_AT_string_length = 0x19
|
DW_AT_string_length = 0x19
|
||||||
DW_AT_common_reference = 0x1a
|
DW_AT_common_reference = 0x1A
|
||||||
DW_AT_comp_dir = 0x1b
|
DW_AT_comp_dir = 0x1B
|
||||||
DW_AT_const_value = 0x1c
|
DW_AT_const_value = 0x1C
|
||||||
DW_AT_containing_type = 0x1d
|
DW_AT_containing_type = 0x1D
|
||||||
DW_AT_default_value = 0x1e
|
DW_AT_default_value = 0x1E
|
||||||
DW_AT_inline = 0x20
|
DW_AT_inline = 0x20
|
||||||
DW_AT_is_optional = 0x21
|
DW_AT_is_optional = 0x21
|
||||||
DW_AT_lower_bound = 0x22
|
DW_AT_lower_bound = 0x22
|
||||||
DW_AT_producer = 0x25
|
DW_AT_producer = 0x25
|
||||||
DW_AT_prototyped = 0x27
|
DW_AT_prototyped = 0x27
|
||||||
DW_AT_return_addr = 0x2a
|
DW_AT_return_addr = 0x2A
|
||||||
DW_AT_start_scope = 0x2c
|
DW_AT_start_scope = 0x2C
|
||||||
DW_AT_bit_stride = 0x2e
|
DW_AT_bit_stride = 0x2E
|
||||||
DW_AT_upper_bound = 0x2f
|
DW_AT_upper_bound = 0x2F
|
||||||
DW_AT_abstract_origin = 0x31
|
DW_AT_abstract_origin = 0x31
|
||||||
DW_AT_accessibility = 0x32
|
DW_AT_accessibility = 0x32
|
||||||
DW_AT_address_class = 0x33
|
DW_AT_address_class = 0x33
|
||||||
@ -133,12 +133,12 @@ DW_AT_calling_convention = 0x36
|
|||||||
DW_AT_count = 0x37
|
DW_AT_count = 0x37
|
||||||
DW_AT_data_member_location = 0x38
|
DW_AT_data_member_location = 0x38
|
||||||
DW_AT_decl_column = 0x39
|
DW_AT_decl_column = 0x39
|
||||||
DW_AT_decl_file = 0x3a
|
DW_AT_decl_file = 0x3A
|
||||||
DW_AT_decl_line = 0x3b
|
DW_AT_decl_line = 0x3B
|
||||||
DW_AT_declaration = 0x3c
|
DW_AT_declaration = 0x3C
|
||||||
DW_AT_discr_list = 0x3d
|
DW_AT_discr_list = 0x3D
|
||||||
DW_AT_encoding = 0x3e
|
DW_AT_encoding = 0x3E
|
||||||
DW_AT_external = 0x3f
|
DW_AT_external = 0x3F
|
||||||
DW_AT_frame_base = 0x40
|
DW_AT_frame_base = 0x40
|
||||||
DW_AT_friend = 0x41
|
DW_AT_friend = 0x41
|
||||||
DW_AT_identifier_case = 0x42
|
DW_AT_identifier_case = 0x42
|
||||||
@ -149,12 +149,12 @@ DW_AT_segment = 0x46
|
|||||||
DW_AT_specification = 0x47
|
DW_AT_specification = 0x47
|
||||||
DW_AT_static_link = 0x48
|
DW_AT_static_link = 0x48
|
||||||
DW_AT_type = 0x49
|
DW_AT_type = 0x49
|
||||||
DW_AT_use_location = 0x4a
|
DW_AT_use_location = 0x4A
|
||||||
DW_AT_variable_parameter = 0x4b
|
DW_AT_variable_parameter = 0x4B
|
||||||
DW_AT_virtuality = 0x4c
|
DW_AT_virtuality = 0x4C
|
||||||
DW_AT_vtable_elem_location = 0x4d
|
DW_AT_vtable_elem_location = 0x4D
|
||||||
DW_AT_allocated = 0x4e
|
DW_AT_allocated = 0x4E
|
||||||
DW_AT_associated = 0x4f
|
DW_AT_associated = 0x4F
|
||||||
DW_AT_data_location = 0x50
|
DW_AT_data_location = 0x50
|
||||||
DW_AT_byte_stride = 0x51
|
DW_AT_byte_stride = 0x51
|
||||||
DW_AT_entry_pc = 0x52
|
DW_AT_entry_pc = 0x52
|
||||||
@ -165,12 +165,12 @@ DW_AT_trampoline = 0x56
|
|||||||
DW_AT_call_column = 0x57
|
DW_AT_call_column = 0x57
|
||||||
DW_AT_call_file = 0x58
|
DW_AT_call_file = 0x58
|
||||||
DW_AT_call_line = 0x59
|
DW_AT_call_line = 0x59
|
||||||
DW_AT_description = 0x5a
|
DW_AT_description = 0x5A
|
||||||
DW_AT_binary_scale = 0x5b
|
DW_AT_binary_scale = 0x5B
|
||||||
DW_AT_decimal_scale = 0x5c
|
DW_AT_decimal_scale = 0x5C
|
||||||
DW_AT_small = 0x5d
|
DW_AT_small = 0x5D
|
||||||
DW_AT_decimal_sign = 0x5e
|
DW_AT_decimal_sign = 0x5E
|
||||||
DW_AT_digit_count = 0x5f
|
DW_AT_digit_count = 0x5F
|
||||||
DW_AT_picture_string = 0x60
|
DW_AT_picture_string = 0x60
|
||||||
DW_AT_mutable = 0x61
|
DW_AT_mutable = 0x61
|
||||||
DW_AT_threads_scaled = 0x62
|
DW_AT_threads_scaled = 0x62
|
||||||
@ -181,12 +181,12 @@ DW_AT_elemental = 0x66
|
|||||||
DW_AT_pure = 0x67
|
DW_AT_pure = 0x67
|
||||||
DW_AT_recursive = 0x68
|
DW_AT_recursive = 0x68
|
||||||
DW_AT_signature = 0x69
|
DW_AT_signature = 0x69
|
||||||
DW_AT_main_subprogram = 0x6a
|
DW_AT_main_subprogram = 0x6A
|
||||||
DW_AT_data_bit_offset = 0x6b
|
DW_AT_data_bit_offset = 0x6B
|
||||||
DW_AT_const_expr = 0x6c
|
DW_AT_const_expr = 0x6C
|
||||||
DW_AT_enum_class = 0x6d
|
DW_AT_enum_class = 0x6D
|
||||||
DW_AT_linkage_name = 0x6e
|
DW_AT_linkage_name = 0x6E
|
||||||
DW_AT_string_length_bit_size = 0x6f
|
DW_AT_string_length_bit_size = 0x6F
|
||||||
DW_AT_string_length_byte_size = 0x70
|
DW_AT_string_length_byte_size = 0x70
|
||||||
DW_AT_rank = 0x71
|
DW_AT_rank = 0x71
|
||||||
DW_AT_str_offsets_base = 0x72
|
DW_AT_str_offsets_base = 0x72
|
||||||
@ -196,12 +196,12 @@ DW_AT_dwo_name = 0x76
|
|||||||
DW_AT_reference = 0x77
|
DW_AT_reference = 0x77
|
||||||
DW_AT_rvalue_reference = 0x78
|
DW_AT_rvalue_reference = 0x78
|
||||||
DW_AT_macros = 0x79
|
DW_AT_macros = 0x79
|
||||||
DW_AT_call_all_calls = 0x7a
|
DW_AT_call_all_calls = 0x7A
|
||||||
DW_AT_call_all_source_calls = 0x7b
|
DW_AT_call_all_source_calls = 0x7B
|
||||||
DW_AT_call_all_tail_calls = 0x7c
|
DW_AT_call_all_tail_calls = 0x7C
|
||||||
DW_AT_call_return_pc = 0x7d
|
DW_AT_call_return_pc = 0x7D
|
||||||
DW_AT_call_value = 0x7e
|
DW_AT_call_value = 0x7E
|
||||||
DW_AT_call_origin = 0x7f
|
DW_AT_call_origin = 0x7F
|
||||||
DW_AT_call_parameter = 0x80
|
DW_AT_call_parameter = 0x80
|
||||||
DW_AT_call_pc = 0x81
|
DW_AT_call_pc = 0x81
|
||||||
DW_AT_call_tail_call = 0x82
|
DW_AT_call_tail_call = 0x82
|
||||||
@ -212,9 +212,9 @@ DW_AT_call_data_value = 0x86
|
|||||||
DW_AT_noreturn = 0x87
|
DW_AT_noreturn = 0x87
|
||||||
DW_AT_alignment = 0x88
|
DW_AT_alignment = 0x88
|
||||||
DW_AT_export_symbols = 0x89
|
DW_AT_export_symbols = 0x89
|
||||||
DW_AT_deleted = 0x8a
|
DW_AT_deleted = 0x8A
|
||||||
DW_AT_defaulted = 0x8b
|
DW_AT_defaulted = 0x8B
|
||||||
DW_AT_loclists_base = 0x8c
|
DW_AT_loclists_base = 0x8C
|
||||||
DW_AT_lo_user = 0x2000
|
DW_AT_lo_user = 0x2000
|
||||||
DW_AT_MIPS_fde = 0x2001
|
DW_AT_MIPS_fde = 0x2001
|
||||||
DW_AT_MIPS_loop_begin = 0x2002
|
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_linkage_name = 0x2007
|
||||||
DW_AT_MIPS_stride = 0x2008
|
DW_AT_MIPS_stride = 0x2008
|
||||||
DW_AT_MIPS_abstract_name = 0x2009
|
DW_AT_MIPS_abstract_name = 0x2009
|
||||||
DW_AT_MIPS_clone_origin = 0x200a
|
DW_AT_MIPS_clone_origin = 0x200A
|
||||||
DW_AT_MIPS_has_inlines = 0x200b
|
DW_AT_MIPS_has_inlines = 0x200B
|
||||||
DW_AT_MIPS_stride_byte = 0x200c
|
DW_AT_MIPS_stride_byte = 0x200C
|
||||||
DW_AT_MIPS_stride_elem = 0x200d
|
DW_AT_MIPS_stride_elem = 0x200D
|
||||||
DW_AT_MIPS_ptr_dopetype = 0x200e
|
DW_AT_MIPS_ptr_dopetype = 0x200E
|
||||||
DW_AT_MIPS_allocatable_dopetype = 0x200f
|
DW_AT_MIPS_allocatable_dopetype = 0x200F
|
||||||
DW_AT_MIPS_assumed_shape_dopetype = 0x2010
|
DW_AT_MIPS_assumed_shape_dopetype = 0x2010
|
||||||
DW_AT_MIPS_assumed_size = 0x2011
|
DW_AT_MIPS_assumed_size = 0x2011
|
||||||
DW_AT_sf_names = 0x2101
|
DW_AT_sf_names = 0x2101
|
||||||
@ -242,12 +242,12 @@ DW_AT_body_end = 0x2106
|
|||||||
DW_AT_GNU_vector = 0x2107
|
DW_AT_GNU_vector = 0x2107
|
||||||
DW_AT_GNU_guarded_by = 0x2108
|
DW_AT_GNU_guarded_by = 0x2108
|
||||||
DW_AT_GNU_pt_guarded_by = 0x2109
|
DW_AT_GNU_pt_guarded_by = 0x2109
|
||||||
DW_AT_GNU_guarded = 0x210a
|
DW_AT_GNU_guarded = 0x210A
|
||||||
DW_AT_GNU_pt_guarded = 0x210b
|
DW_AT_GNU_pt_guarded = 0x210B
|
||||||
DW_AT_GNU_locks_excluded = 0x210c
|
DW_AT_GNU_locks_excluded = 0x210C
|
||||||
DW_AT_GNU_exclusive_locks_required = 0x210d
|
DW_AT_GNU_exclusive_locks_required = 0x210D
|
||||||
DW_AT_GNU_shared_locks_required = 0x210e
|
DW_AT_GNU_shared_locks_required = 0x210E
|
||||||
DW_AT_GNU_odr_signature = 0x210f
|
DW_AT_GNU_odr_signature = 0x210F
|
||||||
DW_AT_GNU_template_name = 0x2110
|
DW_AT_GNU_template_name = 0x2110
|
||||||
DW_AT_GNU_call_site_value = 0x2111
|
DW_AT_GNU_call_site_value = 0x2111
|
||||||
DW_AT_GNU_call_site_data_value = 0x2112
|
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_locviews = 0x2137
|
||||||
DW_AT_GNU_entry_view = 0x2138
|
DW_AT_GNU_entry_view = 0x2138
|
||||||
DW_AT_GNU_macros = 0x2119
|
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_name = 0x2130
|
||||||
DW_AT_GNU_dwo_id = 0x2131
|
DW_AT_GNU_dwo_id = 0x2131
|
||||||
DW_AT_GNU_ranges_base = 0x2132
|
DW_AT_GNU_ranges_base = 0x2132
|
||||||
@ -270,7 +270,7 @@ DW_AT_GNU_pubtypes = 0x2135
|
|||||||
DW_AT_GNU_numerator = 0x2303
|
DW_AT_GNU_numerator = 0x2303
|
||||||
DW_AT_GNU_denominator = 0x2304
|
DW_AT_GNU_denominator = 0x2304
|
||||||
DW_AT_GNU_bias = 0x2305
|
DW_AT_GNU_bias = 0x2305
|
||||||
DW_AT_hi_user = 0x3fff
|
DW_AT_hi_user = 0x3FFF
|
||||||
|
|
||||||
DW_FORM_addr = 0x01
|
DW_FORM_addr = 0x01
|
||||||
DW_FORM_block2 = 0x03
|
DW_FORM_block2 = 0x03
|
||||||
@ -280,12 +280,12 @@ DW_FORM_data4 = 0x06
|
|||||||
DW_FORM_data8 = 0x07
|
DW_FORM_data8 = 0x07
|
||||||
DW_FORM_string = 0x08
|
DW_FORM_string = 0x08
|
||||||
DW_FORM_block = 0x09
|
DW_FORM_block = 0x09
|
||||||
DW_FORM_block1 = 0x0a
|
DW_FORM_block1 = 0x0A
|
||||||
DW_FORM_data1 = 0x0b
|
DW_FORM_data1 = 0x0B
|
||||||
DW_FORM_flag = 0x0c
|
DW_FORM_flag = 0x0C
|
||||||
DW_FORM_sdata = 0x0d
|
DW_FORM_sdata = 0x0D
|
||||||
DW_FORM_strp = 0x0e
|
DW_FORM_strp = 0x0E
|
||||||
DW_FORM_udata = 0x0f
|
DW_FORM_udata = 0x0F
|
||||||
DW_FORM_ref_addr = 0x10
|
DW_FORM_ref_addr = 0x10
|
||||||
DW_FORM_ref1 = 0x11
|
DW_FORM_ref1 = 0x11
|
||||||
DW_FORM_ref2 = 0x12
|
DW_FORM_ref2 = 0x12
|
||||||
@ -296,12 +296,12 @@ DW_FORM_indirect = 0x16
|
|||||||
DW_FORM_sec_offset = 0x17
|
DW_FORM_sec_offset = 0x17
|
||||||
DW_FORM_exprloc = 0x18
|
DW_FORM_exprloc = 0x18
|
||||||
DW_FORM_flag_present = 0x19
|
DW_FORM_flag_present = 0x19
|
||||||
DW_FORM_strx = 0x1a
|
DW_FORM_strx = 0x1A
|
||||||
DW_FORM_addrx = 0x1b
|
DW_FORM_addrx = 0x1B
|
||||||
DW_FORM_ref_sup4 = 0x1c
|
DW_FORM_ref_sup4 = 0x1C
|
||||||
DW_FORM_strp_sup = 0x1d
|
DW_FORM_strp_sup = 0x1D
|
||||||
DW_FORM_data16 = 0x1e
|
DW_FORM_data16 = 0x1E
|
||||||
DW_FORM_line_strp = 0x1f
|
DW_FORM_line_strp = 0x1F
|
||||||
DW_FORM_ref_sig8 = 0x20
|
DW_FORM_ref_sig8 = 0x20
|
||||||
DW_FORM_implicit_const = 0x21
|
DW_FORM_implicit_const = 0x21
|
||||||
DW_FORM_loclistx = 0x22
|
DW_FORM_loclistx = 0x22
|
||||||
@ -312,24 +312,24 @@ DW_FORM_strx2 = 0x26
|
|||||||
DW_FORM_strx3 = 0x27
|
DW_FORM_strx3 = 0x27
|
||||||
DW_FORM_strx4 = 0x28
|
DW_FORM_strx4 = 0x28
|
||||||
DW_FORM_addrx1 = 0x29
|
DW_FORM_addrx1 = 0x29
|
||||||
DW_FORM_addrx2 = 0x2a
|
DW_FORM_addrx2 = 0x2A
|
||||||
DW_FORM_addrx3 = 0x2b
|
DW_FORM_addrx3 = 0x2B
|
||||||
DW_FORM_addrx4 = 0x2c
|
DW_FORM_addrx4 = 0x2C
|
||||||
DW_FORM_GNU_addr_index = 0x1f01
|
DW_FORM_GNU_addr_index = 0x1F01
|
||||||
DW_FORM_GNU_str_index = 0x1f02
|
DW_FORM_GNU_str_index = 0x1F02
|
||||||
DW_FORM_GNU_ref_alt = 0x1f20
|
DW_FORM_GNU_ref_alt = 0x1F20
|
||||||
DW_FORM_GNU_strp_alt = 0x1f21
|
DW_FORM_GNU_strp_alt = 0x1F21
|
||||||
|
|
||||||
DW_OP_addr = 0x03
|
DW_OP_addr = 0x03
|
||||||
DW_OP_deref = 0x06
|
DW_OP_deref = 0x06
|
||||||
DW_OP_const1u = 0x08
|
DW_OP_const1u = 0x08
|
||||||
DW_OP_const1s = 0x09
|
DW_OP_const1s = 0x09
|
||||||
DW_OP_const2u = 0x0a
|
DW_OP_const2u = 0x0A
|
||||||
DW_OP_const2s = 0x0b
|
DW_OP_const2s = 0x0B
|
||||||
DW_OP_const4u = 0x0c
|
DW_OP_const4u = 0x0C
|
||||||
DW_OP_const4s = 0x0d
|
DW_OP_const4s = 0x0D
|
||||||
DW_OP_const8u = 0x0e
|
DW_OP_const8u = 0x0E
|
||||||
DW_OP_const8s = 0x0f
|
DW_OP_const8s = 0x0F
|
||||||
DW_OP_constu = 0x10
|
DW_OP_constu = 0x10
|
||||||
DW_OP_consts = 0x11
|
DW_OP_consts = 0x11
|
||||||
DW_OP_dup = 0x12
|
DW_OP_dup = 0x12
|
||||||
@ -340,12 +340,12 @@ DW_OP_swap = 0x16
|
|||||||
DW_OP_rot = 0x17
|
DW_OP_rot = 0x17
|
||||||
DW_OP_xderef = 0x18
|
DW_OP_xderef = 0x18
|
||||||
DW_OP_abs = 0x19
|
DW_OP_abs = 0x19
|
||||||
DW_OP_and = 0x1a
|
DW_OP_and = 0x1A
|
||||||
DW_OP_div = 0x1b
|
DW_OP_div = 0x1B
|
||||||
DW_OP_minus = 0x1c
|
DW_OP_minus = 0x1C
|
||||||
DW_OP_mod = 0x1d
|
DW_OP_mod = 0x1D
|
||||||
DW_OP_mul = 0x1e
|
DW_OP_mul = 0x1E
|
||||||
DW_OP_neg = 0x1f
|
DW_OP_neg = 0x1F
|
||||||
DW_OP_not = 0x20
|
DW_OP_not = 0x20
|
||||||
DW_OP_or = 0x21
|
DW_OP_or = 0x21
|
||||||
DW_OP_plus = 0x22
|
DW_OP_plus = 0x22
|
||||||
@ -356,12 +356,12 @@ DW_OP_shra = 0x26
|
|||||||
DW_OP_xor = 0x27
|
DW_OP_xor = 0x27
|
||||||
DW_OP_bra = 0x28
|
DW_OP_bra = 0x28
|
||||||
DW_OP_eq = 0x29
|
DW_OP_eq = 0x29
|
||||||
DW_OP_ge = 0x2a
|
DW_OP_ge = 0x2A
|
||||||
DW_OP_gt = 0x2b
|
DW_OP_gt = 0x2B
|
||||||
DW_OP_le = 0x2c
|
DW_OP_le = 0x2C
|
||||||
DW_OP_lt = 0x2d
|
DW_OP_lt = 0x2D
|
||||||
DW_OP_ne = 0x2e
|
DW_OP_ne = 0x2E
|
||||||
DW_OP_skip = 0x2f
|
DW_OP_skip = 0x2F
|
||||||
DW_OP_lit0 = 0x30
|
DW_OP_lit0 = 0x30
|
||||||
DW_OP_lit1 = 0x31
|
DW_OP_lit1 = 0x31
|
||||||
DW_OP_lit2 = 0x32
|
DW_OP_lit2 = 0x32
|
||||||
@ -372,12 +372,12 @@ DW_OP_lit6 = 0x36
|
|||||||
DW_OP_lit7 = 0x37
|
DW_OP_lit7 = 0x37
|
||||||
DW_OP_lit8 = 0x38
|
DW_OP_lit8 = 0x38
|
||||||
DW_OP_lit9 = 0x39
|
DW_OP_lit9 = 0x39
|
||||||
DW_OP_lit10 = 0x3a
|
DW_OP_lit10 = 0x3A
|
||||||
DW_OP_lit11 = 0x3b
|
DW_OP_lit11 = 0x3B
|
||||||
DW_OP_lit12 = 0x3c
|
DW_OP_lit12 = 0x3C
|
||||||
DW_OP_lit13 = 0x3d
|
DW_OP_lit13 = 0x3D
|
||||||
DW_OP_lit14 = 0x3e
|
DW_OP_lit14 = 0x3E
|
||||||
DW_OP_lit15 = 0x3f
|
DW_OP_lit15 = 0x3F
|
||||||
DW_OP_lit16 = 0x40
|
DW_OP_lit16 = 0x40
|
||||||
DW_OP_lit17 = 0x41
|
DW_OP_lit17 = 0x41
|
||||||
DW_OP_lit18 = 0x42
|
DW_OP_lit18 = 0x42
|
||||||
@ -388,12 +388,12 @@ DW_OP_lit22 = 0x46
|
|||||||
DW_OP_lit23 = 0x47
|
DW_OP_lit23 = 0x47
|
||||||
DW_OP_lit24 = 0x48
|
DW_OP_lit24 = 0x48
|
||||||
DW_OP_lit25 = 0x49
|
DW_OP_lit25 = 0x49
|
||||||
DW_OP_lit26 = 0x4a
|
DW_OP_lit26 = 0x4A
|
||||||
DW_OP_lit27 = 0x4b
|
DW_OP_lit27 = 0x4B
|
||||||
DW_OP_lit28 = 0x4c
|
DW_OP_lit28 = 0x4C
|
||||||
DW_OP_lit29 = 0x4d
|
DW_OP_lit29 = 0x4D
|
||||||
DW_OP_lit30 = 0x4e
|
DW_OP_lit30 = 0x4E
|
||||||
DW_OP_lit31 = 0x4f
|
DW_OP_lit31 = 0x4F
|
||||||
DW_OP_reg0 = 0x50
|
DW_OP_reg0 = 0x50
|
||||||
DW_OP_reg1 = 0x51
|
DW_OP_reg1 = 0x51
|
||||||
DW_OP_reg2 = 0x52
|
DW_OP_reg2 = 0x52
|
||||||
@ -404,12 +404,12 @@ DW_OP_reg6 = 0x56
|
|||||||
DW_OP_reg7 = 0x57
|
DW_OP_reg7 = 0x57
|
||||||
DW_OP_reg8 = 0x58
|
DW_OP_reg8 = 0x58
|
||||||
DW_OP_reg9 = 0x59
|
DW_OP_reg9 = 0x59
|
||||||
DW_OP_reg10 = 0x5a
|
DW_OP_reg10 = 0x5A
|
||||||
DW_OP_reg11 = 0x5b
|
DW_OP_reg11 = 0x5B
|
||||||
DW_OP_reg12 = 0x5c
|
DW_OP_reg12 = 0x5C
|
||||||
DW_OP_reg13 = 0x5d
|
DW_OP_reg13 = 0x5D
|
||||||
DW_OP_reg14 = 0x5e
|
DW_OP_reg14 = 0x5E
|
||||||
DW_OP_reg15 = 0x5f
|
DW_OP_reg15 = 0x5F
|
||||||
DW_OP_reg16 = 0x60
|
DW_OP_reg16 = 0x60
|
||||||
DW_OP_reg17 = 0x61
|
DW_OP_reg17 = 0x61
|
||||||
DW_OP_reg18 = 0x62
|
DW_OP_reg18 = 0x62
|
||||||
@ -420,12 +420,12 @@ DW_OP_reg22 = 0x66
|
|||||||
DW_OP_reg23 = 0x67
|
DW_OP_reg23 = 0x67
|
||||||
DW_OP_reg24 = 0x68
|
DW_OP_reg24 = 0x68
|
||||||
DW_OP_reg25 = 0x69
|
DW_OP_reg25 = 0x69
|
||||||
DW_OP_reg26 = 0x6a
|
DW_OP_reg26 = 0x6A
|
||||||
DW_OP_reg27 = 0x6b
|
DW_OP_reg27 = 0x6B
|
||||||
DW_OP_reg28 = 0x6c
|
DW_OP_reg28 = 0x6C
|
||||||
DW_OP_reg29 = 0x6d
|
DW_OP_reg29 = 0x6D
|
||||||
DW_OP_reg30 = 0x6e
|
DW_OP_reg30 = 0x6E
|
||||||
DW_OP_reg31 = 0x6f
|
DW_OP_reg31 = 0x6F
|
||||||
DW_OP_breg0 = 0x70
|
DW_OP_breg0 = 0x70
|
||||||
DW_OP_breg1 = 0x71
|
DW_OP_breg1 = 0x71
|
||||||
DW_OP_breg2 = 0x72
|
DW_OP_breg2 = 0x72
|
||||||
@ -436,12 +436,12 @@ DW_OP_breg6 = 0x76
|
|||||||
DW_OP_breg7 = 0x77
|
DW_OP_breg7 = 0x77
|
||||||
DW_OP_breg8 = 0x78
|
DW_OP_breg8 = 0x78
|
||||||
DW_OP_breg9 = 0x79
|
DW_OP_breg9 = 0x79
|
||||||
DW_OP_breg10 = 0x7a
|
DW_OP_breg10 = 0x7A
|
||||||
DW_OP_breg11 = 0x7b
|
DW_OP_breg11 = 0x7B
|
||||||
DW_OP_breg12 = 0x7c
|
DW_OP_breg12 = 0x7C
|
||||||
DW_OP_breg13 = 0x7d
|
DW_OP_breg13 = 0x7D
|
||||||
DW_OP_breg14 = 0x7e
|
DW_OP_breg14 = 0x7E
|
||||||
DW_OP_breg15 = 0x7f
|
DW_OP_breg15 = 0x7F
|
||||||
DW_OP_breg16 = 0x80
|
DW_OP_breg16 = 0x80
|
||||||
DW_OP_breg17 = 0x81
|
DW_OP_breg17 = 0x81
|
||||||
DW_OP_breg18 = 0x82
|
DW_OP_breg18 = 0x82
|
||||||
@ -452,12 +452,12 @@ DW_OP_breg22 = 0x86
|
|||||||
DW_OP_breg23 = 0x87
|
DW_OP_breg23 = 0x87
|
||||||
DW_OP_breg24 = 0x88
|
DW_OP_breg24 = 0x88
|
||||||
DW_OP_breg25 = 0x89
|
DW_OP_breg25 = 0x89
|
||||||
DW_OP_breg26 = 0x8a
|
DW_OP_breg26 = 0x8A
|
||||||
DW_OP_breg27 = 0x8b
|
DW_OP_breg27 = 0x8B
|
||||||
DW_OP_breg28 = 0x8c
|
DW_OP_breg28 = 0x8C
|
||||||
DW_OP_breg29 = 0x8d
|
DW_OP_breg29 = 0x8D
|
||||||
DW_OP_breg30 = 0x8e
|
DW_OP_breg30 = 0x8E
|
||||||
DW_OP_breg31 = 0x8f
|
DW_OP_breg31 = 0x8F
|
||||||
DW_OP_regx = 0x90
|
DW_OP_regx = 0x90
|
||||||
DW_OP_fbreg = 0x91
|
DW_OP_fbreg = 0x91
|
||||||
DW_OP_bregx = 0x92
|
DW_OP_bregx = 0x92
|
||||||
@ -468,38 +468,38 @@ DW_OP_nop = 0x96
|
|||||||
DW_OP_push_object_address = 0x97
|
DW_OP_push_object_address = 0x97
|
||||||
DW_OP_call2 = 0x98
|
DW_OP_call2 = 0x98
|
||||||
DW_OP_call4 = 0x99
|
DW_OP_call4 = 0x99
|
||||||
DW_OP_call_ref = 0x9a
|
DW_OP_call_ref = 0x9A
|
||||||
DW_OP_form_tls_address = 0x9b
|
DW_OP_form_tls_address = 0x9B
|
||||||
DW_OP_call_frame_cfa = 0x9c
|
DW_OP_call_frame_cfa = 0x9C
|
||||||
DW_OP_bit_piece = 0x9d
|
DW_OP_bit_piece = 0x9D
|
||||||
DW_OP_implicit_value = 0x9e
|
DW_OP_implicit_value = 0x9E
|
||||||
DW_OP_stack_value = 0x9f
|
DW_OP_stack_value = 0x9F
|
||||||
DW_OP_implicit_pointer = 0xa0
|
DW_OP_implicit_pointer = 0xA0
|
||||||
DW_OP_addrx = 0xa1
|
DW_OP_addrx = 0xA1
|
||||||
DW_OP_constx = 0xa2
|
DW_OP_constx = 0xA2
|
||||||
DW_OP_entry_value = 0xa3
|
DW_OP_entry_value = 0xA3
|
||||||
DW_OP_const_type = 0xa4
|
DW_OP_const_type = 0xA4
|
||||||
DW_OP_regval_type = 0xa5
|
DW_OP_regval_type = 0xA5
|
||||||
DW_OP_deref_type = 0xa6
|
DW_OP_deref_type = 0xA6
|
||||||
DW_OP_xderef_type = 0xa7
|
DW_OP_xderef_type = 0xA7
|
||||||
DW_OP_convert = 0xa8
|
DW_OP_convert = 0xA8
|
||||||
DW_OP_reinterpret = 0xa9
|
DW_OP_reinterpret = 0xA9
|
||||||
DW_OP_GNU_push_tls_address = 0xe0
|
DW_OP_GNU_push_tls_address = 0xE0
|
||||||
DW_OP_GNU_uninit = 0xf0
|
DW_OP_GNU_uninit = 0xF0
|
||||||
DW_OP_GNU_encoded_addr = 0xf1
|
DW_OP_GNU_encoded_addr = 0xF1
|
||||||
DW_OP_GNU_implicit_pointer = 0xf2
|
DW_OP_GNU_implicit_pointer = 0xF2
|
||||||
DW_OP_GNU_entry_value = 0xf3
|
DW_OP_GNU_entry_value = 0xF3
|
||||||
DW_OP_GNU_const_type = 0xf4
|
DW_OP_GNU_const_type = 0xF4
|
||||||
DW_OP_GNU_regval_type = 0xf5
|
DW_OP_GNU_regval_type = 0xF5
|
||||||
DW_OP_GNU_deref_type = 0xf6
|
DW_OP_GNU_deref_type = 0xF6
|
||||||
DW_OP_GNU_convert = 0xf7
|
DW_OP_GNU_convert = 0xF7
|
||||||
DW_OP_GNU_reinterpret = 0xf9
|
DW_OP_GNU_reinterpret = 0xF9
|
||||||
DW_OP_GNU_parameter_ref = 0xfa
|
DW_OP_GNU_parameter_ref = 0xFA
|
||||||
DW_OP_GNU_addr_index = 0xfb
|
DW_OP_GNU_addr_index = 0xFB
|
||||||
DW_OP_GNU_const_index = 0xfc
|
DW_OP_GNU_const_index = 0xFC
|
||||||
DW_OP_GNU_variable_value = 0xfd
|
DW_OP_GNU_variable_value = 0xFD
|
||||||
DW_OP_lo_user = 0xe0
|
DW_OP_lo_user = 0xE0
|
||||||
DW_OP_hi_user = 0xff
|
DW_OP_hi_user = 0xFF
|
||||||
|
|
||||||
DW_ATE_void = 0x0
|
DW_ATE_void = 0x0
|
||||||
DW_ATE_address = 0x1
|
DW_ATE_address = 0x1
|
||||||
@ -511,17 +511,17 @@ DW_ATE_signed_char = 0x6
|
|||||||
DW_ATE_unsigned = 0x7
|
DW_ATE_unsigned = 0x7
|
||||||
DW_ATE_unsigned_char = 0x8
|
DW_ATE_unsigned_char = 0x8
|
||||||
DW_ATE_imaginary_float = 0x9
|
DW_ATE_imaginary_float = 0x9
|
||||||
DW_ATE_packed_decimal = 0xa
|
DW_ATE_packed_decimal = 0xA
|
||||||
DW_ATE_numeric_string = 0xb
|
DW_ATE_numeric_string = 0xB
|
||||||
DW_ATE_edited = 0xc
|
DW_ATE_edited = 0xC
|
||||||
DW_ATE_signed_fixed = 0xd
|
DW_ATE_signed_fixed = 0xD
|
||||||
DW_ATE_unsigned_fixed = 0xe
|
DW_ATE_unsigned_fixed = 0xE
|
||||||
DW_ATE_decimal_float = 0xf
|
DW_ATE_decimal_float = 0xF
|
||||||
DW_ATE_UTF = 0x10
|
DW_ATE_UTF = 0x10
|
||||||
DW_ATE_UCS = 0x11
|
DW_ATE_UCS = 0x11
|
||||||
DW_ATE_ASCII = 0x12
|
DW_ATE_ASCII = 0x12
|
||||||
DW_ATE_lo_user = 0x80
|
DW_ATE_lo_user = 0x80
|
||||||
DW_ATE_hi_user = 0xff
|
DW_ATE_hi_user = 0xFF
|
||||||
|
|
||||||
DW_DS_unsigned = 1
|
DW_DS_unsigned = 1
|
||||||
DW_DS_leading_overpunch = 2
|
DW_DS_leading_overpunch = 2
|
||||||
@ -533,7 +533,7 @@ DW_END_default = 0
|
|||||||
DW_END_big = 1
|
DW_END_big = 1
|
||||||
DW_END_little = 2
|
DW_END_little = 2
|
||||||
DW_END_lo_user = 0x40
|
DW_END_lo_user = 0x40
|
||||||
DW_END_hi_user = 0xff
|
DW_END_hi_user = 0xFF
|
||||||
|
|
||||||
DW_ACCESS_public = 1
|
DW_ACCESS_public = 1
|
||||||
DW_ACCESS_protected = 2
|
DW_ACCESS_protected = 2
|
||||||
@ -556,12 +556,12 @@ DW_LANG_Cobol85 = 0x0006
|
|||||||
DW_LANG_Fortran77 = 0x0007
|
DW_LANG_Fortran77 = 0x0007
|
||||||
DW_LANG_Fortran90 = 0x0008
|
DW_LANG_Fortran90 = 0x0008
|
||||||
DW_LANG_Pascal83 = 0x0009
|
DW_LANG_Pascal83 = 0x0009
|
||||||
DW_LANG_Modula2 = 0x000a
|
DW_LANG_Modula2 = 0x000A
|
||||||
DW_LANG_Java = 0x000b
|
DW_LANG_Java = 0x000B
|
||||||
DW_LANG_C99 = 0x000c
|
DW_LANG_C99 = 0x000C
|
||||||
DW_LANG_Ada95 = 0x000d
|
DW_LANG_Ada95 = 0x000D
|
||||||
DW_LANG_Fortran95 = 0x000e
|
DW_LANG_Fortran95 = 0x000E
|
||||||
DW_LANG_PLI = 0x000f
|
DW_LANG_PLI = 0x000F
|
||||||
DW_LANG_ObjC = 0x0010
|
DW_LANG_ObjC = 0x0010
|
||||||
DW_LANG_ObjC_plus_plus = 0x0011
|
DW_LANG_ObjC_plus_plus = 0x0011
|
||||||
DW_LANG_UPC = 0x0012
|
DW_LANG_UPC = 0x0012
|
||||||
@ -572,12 +572,12 @@ DW_LANG_Go = 0x0016
|
|||||||
DW_LANG_Modula3 = 0x0017
|
DW_LANG_Modula3 = 0x0017
|
||||||
DW_LANG_Haskell = 0x0018
|
DW_LANG_Haskell = 0x0018
|
||||||
DW_LANG_C_plus_plus_03 = 0x0019
|
DW_LANG_C_plus_plus_03 = 0x0019
|
||||||
DW_LANG_C_plus_plus_11 = 0x001a
|
DW_LANG_C_plus_plus_11 = 0x001A
|
||||||
DW_LANG_OCaml = 0x001b
|
DW_LANG_OCaml = 0x001B
|
||||||
DW_LANG_Rust = 0x001c
|
DW_LANG_Rust = 0x001C
|
||||||
DW_LANG_C11 = 0x001d
|
DW_LANG_C11 = 0x001D
|
||||||
DW_LANG_Swift = 0x001e
|
DW_LANG_Swift = 0x001E
|
||||||
DW_LANG_Julia = 0x001f
|
DW_LANG_Julia = 0x001F
|
||||||
DW_LANG_Dylan = 0x0020
|
DW_LANG_Dylan = 0x0020
|
||||||
DW_LANG_C_plus_plus_14 = 0x0021
|
DW_LANG_C_plus_plus_14 = 0x0021
|
||||||
DW_LANG_Fortran03 = 0x0022
|
DW_LANG_Fortran03 = 0x0022
|
||||||
@ -586,7 +586,7 @@ DW_LANG_RenderScript = 0x0024
|
|||||||
DW_LANG_BLISS = 0x0025
|
DW_LANG_BLISS = 0x0025
|
||||||
DW_LANG_lo_user = 0x8000
|
DW_LANG_lo_user = 0x8000
|
||||||
DW_LANG_Mips_Assembler = 0x8001
|
DW_LANG_Mips_Assembler = 0x8001
|
||||||
DW_LANG_hi_user = 0xffff
|
DW_LANG_hi_user = 0xFFFF
|
||||||
|
|
||||||
DW_ID_case_sensitive = 0
|
DW_ID_case_sensitive = 0
|
||||||
DW_ID_up_case = 1
|
DW_ID_up_case = 1
|
||||||
@ -599,7 +599,7 @@ DW_CC_nocall = 0x3
|
|||||||
DW_CC_pass_by_reference = 0x4
|
DW_CC_pass_by_reference = 0x4
|
||||||
DW_CC_pass_by_value = 0x5
|
DW_CC_pass_by_value = 0x5
|
||||||
DW_CC_lo_user = 0x40
|
DW_CC_lo_user = 0x40
|
||||||
DW_CC_hi_user = 0xff
|
DW_CC_hi_user = 0xFF
|
||||||
|
|
||||||
DW_INL_not_inlined = 0
|
DW_INL_not_inlined = 0
|
||||||
DW_INL_inlined = 1
|
DW_INL_inlined = 1
|
||||||
@ -622,7 +622,7 @@ DW_LNCT_timestamp = 0x3
|
|||||||
DW_LNCT_size = 0x4
|
DW_LNCT_size = 0x4
|
||||||
DW_LNCT_MD5 = 0x5
|
DW_LNCT_MD5 = 0x5
|
||||||
DW_LNCT_lo_user = 0x2000
|
DW_LNCT_lo_user = 0x2000
|
||||||
DW_LNCT_hi_user = 0x3fff
|
DW_LNCT_hi_user = 0x3FFF
|
||||||
|
|
||||||
DW_LNS_copy = 1
|
DW_LNS_copy = 1
|
||||||
DW_LNS_advance_pc = 2
|
DW_LNS_advance_pc = 2
|
||||||
@ -659,11 +659,11 @@ DW_MACRO_undef_strp = 0x06
|
|||||||
DW_MACRO_import = 0x07
|
DW_MACRO_import = 0x07
|
||||||
DW_MACRO_define_sup = 0x08
|
DW_MACRO_define_sup = 0x08
|
||||||
DW_MACRO_undef_sup = 0x09
|
DW_MACRO_undef_sup = 0x09
|
||||||
DW_MACRO_import_sup = 0x0a
|
DW_MACRO_import_sup = 0x0A
|
||||||
DW_MACRO_define_strx = 0x0b
|
DW_MACRO_define_strx = 0x0B
|
||||||
DW_MACRO_undef_strx = 0x0c
|
DW_MACRO_undef_strx = 0x0C
|
||||||
DW_MACRO_lo_user = 0xe0
|
DW_MACRO_lo_user = 0xE0
|
||||||
DW_MACRO_hi_user = 0xff
|
DW_MACRO_hi_user = 0xFF
|
||||||
|
|
||||||
DW_RLE_end_of_list = 0x0
|
DW_RLE_end_of_list = 0x0
|
||||||
DW_RLE_base_addressx = 0x1
|
DW_RLE_base_addressx = 0x1
|
||||||
@ -691,7 +691,7 @@ DW_LLE_GNU_start_length_entry = 0x3
|
|||||||
|
|
||||||
DW_CFA_advance_loc = 0x40
|
DW_CFA_advance_loc = 0x40
|
||||||
DW_CFA_offset = 0x80
|
DW_CFA_offset = 0x80
|
||||||
DW_CFA_restore = 0xc0
|
DW_CFA_restore = 0xC0
|
||||||
DW_CFA_extended = 0
|
DW_CFA_extended = 0
|
||||||
DW_CFA_nop = 0x00
|
DW_CFA_nop = 0x00
|
||||||
DW_CFA_set_loc = 0x01
|
DW_CFA_set_loc = 0x01
|
||||||
@ -703,12 +703,12 @@ DW_CFA_restore_extended = 0x06
|
|||||||
DW_CFA_undefined = 0x07
|
DW_CFA_undefined = 0x07
|
||||||
DW_CFA_same_value = 0x08
|
DW_CFA_same_value = 0x08
|
||||||
DW_CFA_register = 0x09
|
DW_CFA_register = 0x09
|
||||||
DW_CFA_remember_state = 0x0a
|
DW_CFA_remember_state = 0x0A
|
||||||
DW_CFA_restore_state = 0x0b
|
DW_CFA_restore_state = 0x0B
|
||||||
DW_CFA_def_cfa = 0x0c
|
DW_CFA_def_cfa = 0x0C
|
||||||
DW_CFA_def_cfa_register = 0x0d
|
DW_CFA_def_cfa_register = 0x0D
|
||||||
DW_CFA_def_cfa_offset = 0x0e
|
DW_CFA_def_cfa_offset = 0x0E
|
||||||
DW_CFA_def_cfa_expression = 0x0f
|
DW_CFA_def_cfa_expression = 0x0F
|
||||||
DW_CFA_expression = 0x10
|
DW_CFA_expression = 0x10
|
||||||
DW_CFA_offset_extended_sf = 0x11
|
DW_CFA_offset_extended_sf = 0x11
|
||||||
DW_CFA_def_cfa_sf = 0x12
|
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 = 0x14
|
||||||
DW_CFA_val_offset_sf = 0x15
|
DW_CFA_val_offset_sf = 0x15
|
||||||
DW_CFA_val_expression = 0x16
|
DW_CFA_val_expression = 0x16
|
||||||
DW_CFA_low_user = 0x1c
|
DW_CFA_low_user = 0x1C
|
||||||
DW_CFA_MIPS_advance_loc8 = 0x1d
|
DW_CFA_MIPS_advance_loc8 = 0x1D
|
||||||
DW_CFA_GNU_window_save = 0x2d
|
DW_CFA_GNU_window_save = 0x2D
|
||||||
DW_CFA_GNU_args_size = 0x2e
|
DW_CFA_GNU_args_size = 0x2E
|
||||||
DW_CFA_GNU_negative_offset_extended = 0x2f
|
DW_CFA_GNU_negative_offset_extended = 0x2F
|
||||||
DW_CFA_high_user = 0x3f
|
DW_CFA_high_user = 0x3F
|
||||||
|
|
||||||
DW_CIE_ID_32 = 0xffffffff
|
DW_CIE_ID_32 = 0xFFFFFFFF
|
||||||
DW_CIE_ID_64 = 0xffffffffffffffff
|
DW_CIE_ID_64 = 0xFFFFFFFFFFFFFFFF
|
||||||
|
|
||||||
DW_EH_PE_absptr = 0x00
|
DW_EH_PE_absptr = 0x00
|
||||||
DW_EH_PE_omit = 0xff
|
DW_EH_PE_omit = 0xFF
|
||||||
DW_EH_PE_uleb128 = 0x01
|
DW_EH_PE_uleb128 = 0x01
|
||||||
DW_EH_PE_udata2 = 0x02
|
DW_EH_PE_udata2 = 0x02
|
||||||
DW_EH_PE_udata4 = 0x03
|
DW_EH_PE_udata4 = 0x03
|
||||||
DW_EH_PE_udata8 = 0x04
|
DW_EH_PE_udata8 = 0x04
|
||||||
DW_EH_PE_sleb128 = 0x09
|
DW_EH_PE_sleb128 = 0x09
|
||||||
DW_EH_PE_sdata2 = 0x0a
|
DW_EH_PE_sdata2 = 0x0A
|
||||||
DW_EH_PE_sdata4 = 0x0b
|
DW_EH_PE_sdata4 = 0x0B
|
||||||
DW_EH_PE_sdata8 = 0x0c
|
DW_EH_PE_sdata8 = 0x0C
|
||||||
DW_EH_PE_signed = 0x08
|
DW_EH_PE_signed = 0x08
|
||||||
DW_EH_PE_pcrel = 0x10
|
DW_EH_PE_pcrel = 0x10
|
||||||
DW_EH_PE_textrel = 0x20
|
DW_EH_PE_textrel = 0x20
|
||||||
@ -26,8 +26,10 @@ def section(name: str):
|
|||||||
def wrapper(fn):
|
def wrapper(fn):
|
||||||
fn._section = name
|
fn._section = name
|
||||||
return fn
|
return fn
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
# from types import SimpleNamespace
|
# from types import SimpleNamespace
|
||||||
|
|
||||||
# syscalls = SimpleNamespace(
|
# syscalls = SimpleNamespace(
|
||||||
|
|||||||
17
pythonbpf/expr/__init__.py
Normal file
17
pythonbpf/expr/__init__.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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, access_struct_field
|
||||||
|
from .call_registry import CallHandlerRegistry
|
||||||
|
from .vmlinux_registry import VmlinuxHandlerRegistry
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"eval_expr",
|
||||||
|
"handle_expr",
|
||||||
|
"convert_to_bool",
|
||||||
|
"get_base_type_and_depth",
|
||||||
|
"deref_to_depth",
|
||||||
|
"access_struct_field",
|
||||||
|
"get_operand_value",
|
||||||
|
"CallHandlerRegistry",
|
||||||
|
"VmlinuxHandlerRegistry",
|
||||||
|
]
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user