Use per-arch vmlinux.h

vmlinux.h is not compatible across archs.

Handle this compatibility issue by
* Add arch info into vmlinux.h real file name
* Link vmlinux.h to the target-arch real file at build time
* Use target-arch real file for scx_utils bindgen.

Also refactored clang related logic into a new clang_info mod, which is
shared by bpf_builder.rs and builder.rs.

Signed-off-by: Ming Yang <minos.future@gmail.com>
This commit is contained in:
Ming Yang 2024-10-12 18:13:57 -07:00
parent 03f078ac74
commit a23f3566e3
8 changed files with 234129 additions and 275453 deletions

View File

@ -37,6 +37,12 @@ bindgen = ">=0.68, <0.70"
tar = "0.4"
vergen = { version = "8.0.0", features = ["cargo", "git", "gitcl"] }
walkdir = "2.4"
anyhow = "1.0.65"
glob = "0.3"
libbpf-cargo = "0.24.1"
sscanf = "0.4"
lazy_static = "1.4"
version-compare = "0.1"
[features]
default = []

View File

@ -3,43 +3,18 @@
// This software may be used and distributed according to the terms of the
// GNU General Public License version 2.
use crate::clang_info::ClangInfo;
use anyhow::anyhow;
use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use glob::glob;
use libbpf_cargo::SkeletonBuilder;
use sscanf::sscanf;
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
lazy_static::lazy_static! {
// Map clang archs to the __TARGET_ARCH list in
// tools/lib/bpf/bpf_tracing.h in the kernel tree.
static ref ARCH_MAP: HashMap<&'static str, &'static str> = vec![
("x86", "x86"),
("x86_64", "x86"),
("s390", "s390"),
("s390x", "s390"),
("arm", "arm"),
("aarch64", "arm64"),
("mips", "mips"),
("mips64", "mips"),
("ppc32", "powerpc"),
("ppc64", "powerpc"),
("ppc64le", "powerpc"),
("sparc", "sparc"),
("sparcv9", "sparc"),
("riscv32", "riscv"),
("riscv64", "riscv"),
("arc", "arc"), // unsure this is supported
("loongarch64", "loongarch"), // ditto
].into_iter().collect();
}
#[derive(Debug)]
/// # Build helpers for sched_ext schedulers with Rust userspace component
@ -209,7 +184,7 @@ lazy_static::lazy_static! {
/// -L$KERNEL/tools/bpf/bpftool/libbpf" cargo build --release
/// ```
pub struct BpfBuilder {
clang: (String, String, String), // (clang, ver, arch)
clang: ClangInfo,
cflags: Vec<String>,
out_dir: PathBuf,
@ -219,140 +194,6 @@ pub struct BpfBuilder {
}
impl BpfBuilder {
fn skip_clang_version_prefix(line: &str) -> &str {
if let Some(index) = line.find("clang version") {
&line[index..]
} else {
line
}
}
fn find_clang() -> Result<(String, String, String)> {
let clang = env::var("BPF_CLANG").unwrap_or("clang".into());
let output = Command::new(&clang)
.args(["--version"])
.output()
.with_context(|| format!("Failed to run \"{} --version\"", &clang))?;
let stdout = String::from_utf8(output.stdout)?;
let (mut ver, mut arch) = (None, None);
for line in stdout.lines() {
if let Ok(v) = sscanf!(
Self::skip_clang_version_prefix(line),
"clang version {String}"
) {
// Version could be followed by (URL SHA1). Only take
// the first word.
ver = Some(v.split_whitespace().next().unwrap().to_string());
continue;
}
if let Ok(v) = sscanf!(line, "Target: {String}") {
arch = Some(v.split('-').next().unwrap().to_string());
continue;
}
}
let (ver, arch) = (
ver.ok_or(anyhow!("Failed to read clang version"))?,
arch.ok_or(anyhow!("Failed to read clang target arch"))?,
);
if version_compare::compare(&ver, "16") == Ok(version_compare::Cmp::Lt) {
bail!(
"clang < 16 loses high 32 bits of 64 bit enums when compiling BPF ({:?} ver={:?})",
&clang,
&ver
);
}
if version_compare::compare(&ver, "17") == Ok(version_compare::Cmp::Lt) {
println!(
"cargo:warning=clang >= 17 recommended ({:?} ver={:?})",
&clang, &ver
);
}
Ok((clang, ver, arch))
}
fn determine_base_cflags(
(clang, _ver, arch): &(String, String, String),
) -> Result<Vec<String>> {
// Determine kernel target arch.
let kernel_target = match ARCH_MAP.get(arch.as_str()) {
Some(v) => v,
None => bail!("CPU arch {:?} not found in ARCH_MAP", &arch),
};
// Determine system includes.
let output = Command::new(&clang)
.args(["-v", "-E", "-"])
.output()
.with_context(|| format!("Failed to run \"{} -v -E - < /dev/null", &clang))?;
let stderr = String::from_utf8(output.stderr)?;
let mut sys_incls = None;
for line in stderr.lines() {
if line == "#include <...> search starts here:" {
sys_incls = Some(vec![]);
continue;
}
if sys_incls.is_none() {
continue;
}
if line == "End of search list." {
break;
}
sys_incls.as_mut().unwrap().push(line.trim());
}
let sys_incls = match sys_incls {
Some(v) => v,
None => bail!("Failed to find system includes from {:?}", &clang),
};
// Determine endian.
let output = Command::new(&clang)
.args(["-dM", "-E", "-"])
.output()
.with_context(|| format!("Failed to run \"{} -dM E - < /dev/null", &clang))?;
let stdout = String::from_utf8(output.stdout)?;
let mut endian = None;
for line in stdout.lines() {
match sscanf!(line, "#define __BYTE_ORDER__ {str}") {
Ok(v) => {
endian = Some(match v {
"__ORDER_LITTLE_ENDIAN__" => "little",
"__ORDER_BIG_ENDIAN__" => "big",
v => bail!("Unknown __BYTE_ORDER__ {:?}", v),
});
break;
}
_ => {}
}
}
let endian = match endian {
Some(v) => v,
None => bail!("Failed to find __BYTE_ORDER__ from {:?}", &clang),
};
// Assemble cflags.
let mut cflags: Vec<String> = ["-g", "-O2", "-Wall", "-Wno-compare-distinct-pointer-types"]
.into_iter()
.map(|x| x.into())
.collect();
cflags.push(format!("-D__TARGET_ARCH_{}", &kernel_target));
cflags.push("-mcpu=v3".into());
cflags.push(format!("-m{}-endian", endian));
cflags.append(
&mut sys_incls
.into_iter()
.flat_map(|x| ["-idirafter".into(), x.into()])
.collect(),
);
Ok(cflags)
}
const BPF_H_TAR: &'static [u8] = include_bytes!(concat!(env!("OUT_DIR"), "/bpf_h.tar"));
fn install_bpf_h<P: AsRef<Path>>(dest: P) -> Result<()> {
@ -361,30 +202,36 @@ impl BpfBuilder {
Ok(())
}
/// Return `(VER, SHA1)` from which the bulit-in `vmlinux.h` is generated.
pub fn vmlinux_h_ver_sha1() -> (String, String) {
let mut ar = tar::Archive::new(Self::BPF_H_TAR);
fn ln_vmlinux_h<P: AsRef<Path> + AsRef<OsStr>>(dest: P, kernel_target: &str) -> Result<()> {
let entries = fs::read_dir(&dest)?;
for file in ar.entries().unwrap() {
let file = file.unwrap();
if file.header().path().unwrap() != Path::new("vmlinux/vmlinux.h") {
continue;
for entry in entries {
let entry = entry?;
let file_name = entry.file_name();
let file_name_str = file_name.to_string_lossy();
if file_name_str.contains(&kernel_target) {
let source = entry.path();
let destination = Path::new(&dest).join("vmlinux.h");
// Create symlink to the matched file
if destination.exists() {
fs::remove_file(&destination)?;
}
std::os::unix::fs::symlink(&source, &destination)?;
println!(
"Created symlink from {} to {}",
source.display(),
destination.display()
);
return Ok(());
}
let name = file
.link_name()
.unwrap()
.unwrap()
.to_string_lossy()
.to_string();
return sscanf!(name, "vmlinux-v{String}-g{String}.h").unwrap();
}
panic!("vmlinux/vmlinux.h not found");
Err(anyhow!("vmlinux.h for arch {} is not found", kernel_target))
}
fn determine_cflags<P>(clang: &(String, String, String), out_dir: P) -> Result<Vec<String>>
fn determine_cflags<P>(clang: &ClangInfo, out_dir: P) -> Result<Vec<String>>
where
P: AsRef<Path> + std::fmt::Debug,
{
@ -399,11 +246,18 @@ impl BpfBuilder {
.to_string();
Self::install_bpf_h(&bpf_h)?;
let vmlinux_dir = Path::new(&bpf_h)
.join("vmlinux")
.to_str()
.ok_or(anyhow!("{:?}/vmlinux can't be converted to str", &bpf_h))?
.to_string();
Self::ln_vmlinux_h(&vmlinux_dir, &clang.kernel_target()?)?;
let mut cflags = Vec::<String>::new();
cflags.append(&mut match env::var("BPF_BASE_CFLAGS") {
Ok(v) => v.split_whitespace().map(|x| x.into()).collect(),
_ => Self::determine_base_cflags(&clang)?,
_ => clang.determine_base_cflags()?,
});
cflags.append(&mut match env::var("BPF_EXTRA_CFLAGS_PRE_INCL") {
@ -429,7 +283,7 @@ impl BpfBuilder {
pub fn new() -> Result<Self> {
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
let clang = Self::find_clang()?;
let clang = ClangInfo::new()?;
let cflags = match env::var("BPF_CFLAGS") {
Ok(v) => v.split_whitespace().map(|x| x.into()).collect(),
_ => Self::determine_cflags(&clang, &out_dir)?,
@ -527,7 +381,7 @@ impl BpfBuilder {
SkeletonBuilder::new()
.source(input)
.obj(&obj)
.clang(&self.clang.0)
.clang(&self.clang.clang)
.clang_args(&self.cflags)
.build_and_generate(&skel_path)?;
@ -578,6 +432,10 @@ impl BpfBuilder {
#[cfg(test)]
mod tests {
use sscanf::sscanf;
use crate::builder::ClangInfo;
#[test]
fn test_bpf_builder_new() {
let res = super::BpfBuilder::new();
@ -586,15 +444,41 @@ mod tests {
#[test]
fn test_vmlinux_h_ver_sha1() {
let (ver, sha1) = super::BpfBuilder::vmlinux_h_ver_sha1();
let clang_info = ClangInfo::new().unwrap();
println!("vmlinux.h: ver={:?} sha1={:?}", &ver, &sha1,);
let mut ar = tar::Archive::new(super::BpfBuilder::BPF_H_TAR);
let mut found = false;
assert!(regex::Regex::new(r"^([1-9][0-9]*\.[1-9][0-9][a-z0-9-]*)$")
.unwrap()
.is_match(&ver));
assert!(regex::Regex::new(r"^[0-9a-z]{12}$")
.unwrap()
.is_match(&sha1));
for entry in ar.entries().unwrap() {
let entry = entry.unwrap();
let file_name = entry.header().path().unwrap();
let file_name_str = file_name.to_string_lossy().to_owned();
if file_name_str.contains(&clang_info.kernel_target().unwrap()) {
found = true;
} else if !file_name_str.contains("vmlinux/vmlinux") {
continue;
}
println!("checking {file_name_str}");
let (arch, ver, sha1) = sscanf!(
file_name_str,
"vmlinux/vmlinux-{String}-v{String}-g{String}.h"
)
.unwrap();
println!(
"vmlinux.h: arch={:?} ver={:?} sha1={:?}",
&arch, &ver, &sha1,
);
assert!(regex::Regex::new(r"^([1-9][0-9]*\.[1-9][0-9][a-z0-9-]*)$")
.unwrap()
.is_match(&ver));
assert!(regex::Regex::new(r"^[0-9a-z]{12}$")
.unwrap()
.is_match(&sha1));
}
assert!(found);
}
}

View File

@ -3,9 +3,13 @@
// This software may be used and distributed according to the terms of the
// GNU General Public License version 2.
use std::env;
use std::fs::File;
use std::path::PathBuf;
use std::{
ffi::OsStr,
fs::{self, File},
path::{Path, PathBuf},
};
include!("clang_info.rs");
const BPF_H: &str = "bpf_h";
@ -33,8 +37,37 @@ impl Builder {
}
}
fn find_vmlinux_h<P: AsRef<Path> + AsRef<OsStr>>(
dest: P,
kernel_target: &str,
) -> Result<String> {
let entries = fs::read_dir(&dest)?;
for entry in entries {
let entry = entry?;
let file_name = entry.file_name();
let file_name_str = file_name.to_string_lossy();
println!("{file_name_str}");
if file_name_str.contains(&kernel_target) {
return Ok(entry.path().to_string_lossy().into_owned());
}
}
Err(anyhow!("vmlinux.h for arch {} is not found", kernel_target))
}
fn gen_bindings(&self) {
let out_dir = env::var("OUT_DIR").unwrap();
let clang = ClangInfo::new().unwrap();
let vmlinux_dir = Path::new(&BPF_H)
.join("vmlinux")
.to_str()
.ok_or(anyhow!("{:?}/vimlinux can't be converted to str", BPF_H))
.unwrap()
.to_string();
let vmlinux_h =
Self::find_vmlinux_h(&vmlinux_dir, &clang.kernel_target().unwrap()).unwrap();
// FIXME - bindgen's API changed between 0.68 and 0.69 so that
// `bindgen::CargoCallbacks::new()` should be used instead of
// `bindgen::CargoCallbacks`. Unfortunately, as of Dec 2023, fedora is
@ -43,7 +76,7 @@ impl Builder {
// fedora can be updated to bindgen >= 0.69.
#[allow(deprecated)]
let bindings = bindgen::Builder::default()
.header("bindings.h")
.header(vmlinux_h)
.allowlist_type("scx_exit_kind")
.allowlist_type("scx_consts")
.parse_callbacks(Box::new(bindgen::CargoCallbacks))

View File

@ -0,0 +1,174 @@
use std::{collections::HashMap, env, process::Command};
use anyhow::{anyhow, bail, Context, Result};
use sscanf::sscanf;
lazy_static::lazy_static! {
// Map clang archs to the __TARGET_ARCH list in
// tools/lib/bpf/bpf_tracing.h in the kernel tree.
static ref ARCH_MAP: HashMap<&'static str, &'static str> = vec![
("x86", "x86"),
("x86_64", "x86"),
("s390", "s390"),
("s390x", "s390"),
("arm", "arm"),
("aarch64", "arm64"),
("mips", "mips"),
("mips64", "mips"),
("ppc32", "powerpc"),
("ppc64", "powerpc"),
("ppc64le", "powerpc"),
("sparc", "sparc"),
("sparcv9", "sparc"),
("riscv32", "riscv"),
("riscv64", "riscv"),
("arc", "arc"), // unsure this is supported
("loongarch64", "loongarch"), // ditto
].into_iter().collect();
}
#[derive(Debug)]
#[allow(dead_code)]
pub struct ClangInfo {
pub clang: String,
pub ver: String,
pub arch: String,
}
impl ClangInfo {
pub fn new() -> Result<ClangInfo> {
let clang = env::var("BPF_CLANG").unwrap_or("clang".into());
let output = Command::new(&clang)
.args(["--version"])
.output()
.with_context(|| format!("Failed to run \"{} --version\"", &clang))?;
let stdout = String::from_utf8(output.stdout)?;
let (mut ver, mut arch) = (None, None);
for line in stdout.lines() {
if let Ok(v) = sscanf!(
Self::skip_clang_version_prefix(line),
"clang version {String}"
) {
// Version could be followed by (URL SHA1). Only take
// the first word.
ver = Some(v.split_whitespace().next().unwrap().to_string());
continue;
}
if let Ok(v) = sscanf!(line, "Target: {String}") {
arch = Some(v.split('-').next().unwrap().to_string());
continue;
}
}
let (ver, arch) = (
ver.ok_or(anyhow!("Failed to read clang version"))?,
arch.ok_or(anyhow!("Failed to read clang target arch"))?,
);
if version_compare::compare(&ver, "16") == Ok(version_compare::Cmp::Lt) {
bail!(
"clang < 16 loses high 32 bits of 64 bit enums when compiling BPF ({:?} ver={:?})",
&clang,
&ver
);
}
if version_compare::compare(&ver, "17") == Ok(version_compare::Cmp::Lt) {
println!(
"cargo:warning=clang >= 17 recommended ({:?} ver={:?})",
&clang, &ver
);
}
Ok(ClangInfo { clang, ver, arch })
}
fn skip_clang_version_prefix(line: &str) -> &str {
if let Some(index) = line.find("clang version") {
&line[index..]
} else {
line
}
}
pub fn kernel_target(&self) -> Result<String> {
// Determine kernel target arch.
match ARCH_MAP.get(self.arch.as_str()) {
Some(v) => Ok(v.to_string()),
None => Err(anyhow!("CPU arch {} not found in ARCH_MAP", self.arch)),
}
}
#[allow(dead_code)] // for it is not used during build script execution
pub fn determine_base_cflags(&self) -> Result<Vec<String>> {
let kernel_target = self.kernel_target()?;
// Determine system includes.
let output = Command::new(&self.clang)
.args(["-v", "-E", "-"])
.output()
.with_context(|| format!("Failed to run \"{} -v -E - < /dev/null", self.clang))?;
let stderr = String::from_utf8(output.stderr)?;
let mut sys_incls = None;
for line in stderr.lines() {
if line == "#include <...> search starts here:" {
sys_incls = Some(vec![]);
continue;
}
if sys_incls.is_none() {
continue;
}
if line == "End of search list." {
break;
}
sys_incls.as_mut().unwrap().push(line.trim());
}
let sys_incls = match sys_incls {
Some(v) => v,
None => bail!("Failed to find system includes from {:?}", self.clang),
};
// Determine endian.
let output = Command::new(&self.clang)
.args(["-dM", "-E", "-"])
.output()
.with_context(|| format!("Failed to run \"{} -dM E - < /dev/null", self.clang))?;
let stdout = String::from_utf8(output.stdout)?;
let mut endian = None;
for line in stdout.lines() {
match sscanf!(line, "#define __BYTE_ORDER__ {str}") {
Ok(v) => {
endian = Some(match v {
"__ORDER_LITTLE_ENDIAN__" => "little",
"__ORDER_BIG_ENDIAN__" => "big",
v => bail!("Unknown __BYTE_ORDER__ {:?}", v),
});
break;
}
_ => {}
}
}
let endian = match endian {
Some(v) => v,
None => bail!("Failed to find __BYTE_ORDER__ from {:?}", self.clang),
};
// Assemble cflags.
let mut cflags: Vec<String> = ["-g", "-O2", "-Wall", "-Wno-compare-distinct-pointer-types"]
.into_iter()
.map(|x| x.into())
.collect();
cflags.push(format!("-D__TARGET_ARCH_{}", &kernel_target));
cflags.push("-mcpu=v3".into());
cflags.push(format!("-m{}-endian", endian));
cflags.append(
&mut sys_incls
.into_iter()
.flat_map(|x| ["-idirafter".into(), x.into()])
.collect(),
);
Ok(cflags)
}
}

View File

@ -33,6 +33,8 @@
pub use log::warn;
pub use paste::paste;
mod clang_info;
mod bindings;
mod bpf_builder;

View File

@ -1 +0,0 @@
vmlinux-v6.12-rc0-ga748db0c8c6a.h