mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
676 lines
24 KiB
Markdown
676 lines
24 KiB
Markdown
# Nvidia Driver Differ Tool
|
||||
|
|
|
|||
|
|
Status as of 2024-08-14: Completed. To get an overview of what was ultimately
|
|||
|
|
implemented, check out the
|
|||
|
|
[presentation here](https://github.com/google/gvisor/blob/master/g3doc/presentations/nvidia_tooling.pdf).
|
|||
|
|
|
|||
|
|
## Overview
|
|||
|
|
|
|||
|
|
This tool is intended to make it easier to support new Nvidia driver versions
|
|||
|
|
within nvproxy. Any new version of an Nvidia driver can come with changes to its
|
|||
|
|
ioctl structs, and if nvproxy supports those structs, it will have to copy those
|
|||
|
|
changes as well. Prior to the implementation of this proposal, however, finding
|
|||
|
|
those changes is both difficult and tedious. This tool attempts to automate the
|
|||
|
|
bulk of this work.
|
|||
|
|
|
|||
|
|
This document goes over some design proposals of how this tool should be built,
|
|||
|
|
especially how it should interface with nvproxy.
|
|||
|
|
|
|||
|
|
## Problem Statement
|
|||
|
|
|
|||
|
|
Let's say we want to add support for a new driver version B. At a high level,
|
|||
|
|
the work of this tool can be broken into the following steps:
|
|||
|
|
|
|||
|
|
1. Find the nearest ancestor version, A, that nvproxy supports. nvproxy
|
|||
|
|
supports multiple major version numbers, so these versions form a tree
|
|||
|
|
instead of a simple line of dependencies.
|
|||
|
|
2. Get the list of currently used structs in nvproxy for version A.
|
|||
|
|
3. For each struct, compare its definition in versions A and B. Report any
|
|||
|
|
differences–type, number, and ordering of fields all matter.
|
|||
|
|
|
|||
|
|
The biggest roadblock to implementing this is that the only immediate
|
|||
|
|
information reported by nvproxy is the ioctl calls it supports. Some ioctl calls
|
|||
|
|
have corresponding structs defined, but:
|
|||
|
|
|
|||
|
|
- There is no way to directly get this mapping; it would require analysis of
|
|||
|
|
the nvproxy code with some AST parser.
|
|||
|
|
- Some ioctls have multiple structs defined due to changes across version; we
|
|||
|
|
need some way to know which struct nvproxy is using for version A.
|
|||
|
|
- Some ioctls simply don't have struct definitions written out, since they're
|
|||
|
|
simply passed by copying a given `size` bytes. Most control commands are
|
|||
|
|
like this.
|
|||
|
|
|
|||
|
|
Additionally, nested structs are also a concern. For example, some structs may
|
|||
|
|
be defined like so:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
type IOCTL_FOO struct {
|
|||
|
|
Foo uint32
|
|||
|
|
Bars [MAX_BARS]IOCTL_BAR
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type IOCTL_BAR struct {
|
|||
|
|
Bar uint32
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
This means we not only have to map ioctl calls to their corresponding structs,
|
|||
|
|
but also (recursively) parse their fields to see if there are nested structs.
|
|||
|
|
|
|||
|
|
## Proposal
|
|||
|
|
|
|||
|
|
We can split this tool into two parts. The first part can be a Go tool that is
|
|||
|
|
built against nvproxy and finds the list of structs for version B. Once we have
|
|||
|
|
a specific list of structs to look up, we can pass that to a C++ tool that uses
|
|||
|
|
[Clang's C++ AST Matcher API](https://clang.llvm.org/docs/LibASTMatchersReference.html)
|
|||
|
|
to find the corresponding struct definitions in the driver source code. These
|
|||
|
|
definitions are then passed back to the Go tool, which does the necessary
|
|||
|
|
diffing and reporting back to the user.
|
|||
|
|
|
|||
|
|
### Fetching struct names from nvproxy
|
|||
|
|
|
|||
|
|
The primary problem to tackle on the Go side is how to get the list of struct
|
|||
|
|
names nvproxy depends on for a given version B. Since this system should allow
|
|||
|
|
for versioning of these struct names, we can extend the existing `driverABI`
|
|||
|
|
struct to include this information.
|
|||
|
|
|
|||
|
|
However, almost every normal use case of `driverABI` will not need to use these
|
|||
|
|
names, and they should not be sitting around wasting memory. Thus, we can add a
|
|||
|
|
`getStructNames` function to `driverABI` that will construct and return the list
|
|||
|
|
of relevant names only when they are needed. It should look like this:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
type driverABI struct {
|
|||
|
|
frontendIoctl map[uint32]frontendIoctlHandler
|
|||
|
|
uvmIoctl map[uint32]uvmIoctlHandler
|
|||
|
|
controlCmd map[uint32]controlCmdHandler
|
|||
|
|
allocationClass map[nvgpu.ClassID]allocationClassHandler
|
|||
|
|
|
|||
|
|
getStructNames driverStructNamesFunc
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type driverStructNamesFunc func() *driverStructNames
|
|||
|
|
|
|||
|
|
type driverStructNames struct {
|
|||
|
|
frontendNames map[uint32][]string
|
|||
|
|
uvmNames map[uint32][]string
|
|||
|
|
controlNames map[uint32][]string
|
|||
|
|
allocationNames map[nvgpu.ClassID][]string
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The fields in `driverStructNames` map every ioctl to a list of struct names that
|
|||
|
|
it depends on (this is a list to support the case of nested structs). By
|
|||
|
|
explicitly mapping each struct name to their corresponding ioctl, it should make
|
|||
|
|
this list easy to maintain. We can compare against the ioctls included in the
|
|||
|
|
ABI map to ensure every ioctl call is accounted for in each version. It also
|
|||
|
|
makes it easier to modify definitions for a specific ioctl number due to a
|
|||
|
|
version change.
|
|||
|
|
|
|||
|
|
There are a few cases to consider when generating the list of names for an
|
|||
|
|
ioctl:
|
|||
|
|
|
|||
|
|
- For ioctls with a struct defined in nvproxy, we can provide a function
|
|||
|
|
`getStructName(any)` that takes a struct and returns its corresponding
|
|||
|
|
driver name in a `[]string`. How this should be done is discussed further
|
|||
|
|
below.
|
|||
|
|
- For ioctls without a struct defined in nvproxy, we can directly write the
|
|||
|
|
corresponding struct names. This can be done with a function
|
|||
|
|
`simpleIoctl(name)` that simply returns a `[]string` with one element, to
|
|||
|
|
make it more explicit.
|
|||
|
|
- Finally, there are some ioctls (or maybe just `NV_ESC_RM_ALLOC`) that allow
|
|||
|
|
multiple types of parameters. In this case, corresponding lists for each
|
|||
|
|
parameter type can be merged.
|
|||
|
|
|
|||
|
|
Concretely, this would look something like this:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
driverStructNames{
|
|||
|
|
frontendNames: map[uint32][]string{
|
|||
|
|
NV_ESC_RM_ALLOC_MEMORY: append(getStructName(NVOS21Parameters{}), getStructName(NVOS64Parameters{})...),
|
|||
|
|
},
|
|||
|
|
uvmNames: map[uint32][]string{
|
|||
|
|
UVM_ALLOC_SEMAPHORE_POOL: getStructName(UVM_ALLOC_SEMAPHORE_POOL_PARAMS{})
|
|||
|
|
},
|
|||
|
|
controlCmd: map[uint32][]string{
|
|||
|
|
NV2080_CTRL_CMD_GPU_GET_NAME_STRING: simpleIoctl("NV2080_CTRL_GPU_GET_NAME_STRING_PARAMS"),
|
|||
|
|
},
|
|||
|
|
allocationNames: map[nvgpu.ClassID][]string{
|
|||
|
|
NV01_MEMORY_SYSTEM: getStructName(NV_MEMORY_ALLOCATION_PARAMS{}),
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Looking specifically now at `getStructName`, there are a few ways in which it
|
|||
|
|
can be implemented:
|
|||
|
|
|
|||
|
|
1. We can require that struct names in nvproxy are exactly the same as their
|
|||
|
|
counterpart in the Nvidia driver. This way, Go's
|
|||
|
|
[reflect](https://pkg.go.dev/reflect) package can be used to simply read the
|
|||
|
|
name of the struct being passed in.
|
|||
|
|
|
|||
|
|
To handle versioning changes, we can agree on some suffix format. For
|
|||
|
|
example, everything after a double underscore is ignored. This way, both
|
|||
|
|
`PARAMS` and `PARAMS__V550` can be defined.
|
|||
|
|
|
|||
|
|
2. We can introduce struct tags that specify the name of the corresponding
|
|||
|
|
struct in the Nvidia driver code, which would always sit on the first field.
|
|||
|
|
This could look something like this:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
type IOCTL_FOO_V550 struct {
|
|||
|
|
Foo uint32 `nvproxy:"ioctl_foo"`
|
|||
|
|
Bars [MAX_BARS]IOCTL_BAR
|
|||
|
|
Baz uint64
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
This struct tag can be read using reflect. For structs that are named the
|
|||
|
|
same between nvproxy and the Nvidia driver, we can also have a convenient
|
|||
|
|
`nvproxy:"same"` case that simply uses the struct’s name.
|
|||
|
|
|
|||
|
|
3. Instead of using a struct tag, we can use a struct comment similar to `//
|
|||
|
|
+marshal` or `// +stateify`. An external tool would then run on the nvproxy
|
|||
|
|
package, find each struct with the struct comment, and implement an
|
|||
|
|
interface that reports back the corresponding driver name.
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
type NvidiaDriverStruct interface {
|
|||
|
|
func GetDriverName() string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// +nvproxy ioctl_foo
|
|||
|
|
type IOCTL_FOO_V550 struct {
|
|||
|
|
Foo uint32
|
|||
|
|
Bars [MAX_BARS]IOCTL_BAR
|
|||
|
|
Baz uint64
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Auto-generated
|
|||
|
|
func (s IOCTL_FOO_V550) GetDriverName() string {
|
|||
|
|
return "ioctl_foo"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The use of an external tool makes this method a lot more involved, and
|
|||
|
|
potentially expensive to maintain. The main benefit is that it is a better
|
|||
|
|
convention than requiring tags on the first field. The code will also be
|
|||
|
|
similar to `go_marshal` or `go_stateify`, so a lot could be copied over.
|
|||
|
|
Specifically, the code generation step and the code to collect all annotated
|
|||
|
|
types in `Generator.collectMarshallableTypes` can be the same.
|
|||
|
|
|
|||
|
|
Comparing these three ideas, numbers 1 and 2 are definitely the easiest to
|
|||
|
|
implement. Idea 2 will be more robust as well, since we don’t have to worry
|
|||
|
|
about Nvidia driver structs potentially having double underscores or whatever
|
|||
|
|
separator we decide on. In the end, idea 2 was implemented; if it is important
|
|||
|
|
to maintaining convention, idea 3 can still be implemented afterwards.
|
|||
|
|
|
|||
|
|
There is also the problem of nested structs that needs to be addressed. Although
|
|||
|
|
the Go side can try and tackle this problem, it would be hard to maintain for
|
|||
|
|
the simple structs that are not defined in nvproxy, as we would have to manually
|
|||
|
|
check if they have nested structs and write down what they are. Thus, it would
|
|||
|
|
be easier to make the C++ Clang tool do this, and simply have the Go tool find
|
|||
|
|
the list of all top-level structs.
|
|||
|
|
|
|||
|
|
### C++ Clang parser
|
|||
|
|
|
|||
|
|
After gathering a list of struct names to verify, this tool can locally clone
|
|||
|
|
the code for both versions A and B. From here, Clang's C++ AST Matcher API can
|
|||
|
|
be used to generate an AST and find the struct definitions given the name.
|
|||
|
|
|
|||
|
|
The Clang API includes the ability to quickly set up command line tools to run
|
|||
|
|
the AST matcher; this
|
|||
|
|
[tutorial](https://clang.llvm.org/docs/LibASTMatchersTutorial.html) in the
|
|||
|
|
documentation covers everything this tool needs to do. Out of the box, it takes
|
|||
|
|
in a source file, and allows you to run any set of matchers on that source file.
|
|||
|
|
This means we can create a small C++ file that `#include`s all the header files
|
|||
|
|
that contain struct definitions, similar to what
|
|||
|
|
[Geohot does with his sniffer](https://github.com/geohot/cuda_ioctl_sniffer/blob/master/pstruct/include.cc).
|
|||
|
|
Clang will automatically expand these `#include`s, so any struct defined in
|
|||
|
|
there will be matchable.
|
|||
|
|
|
|||
|
|
In the driver source code, all structs are named via a `typedef`. This means the
|
|||
|
|
tool should try and match against a `typedef` with a given struct name, and then
|
|||
|
|
look at the struct type aliases. This is done with the following Clang matcher
|
|||
|
|
expression:
|
|||
|
|
|
|||
|
|
```c++
|
|||
|
|
typedefDecl(
|
|||
|
|
allOf(
|
|||
|
|
hasName(struct_name),
|
|||
|
|
// Match and bind to the struct declaration.
|
|||
|
|
hasType(
|
|||
|
|
// Need to specify elaboratedType, otherwise hasType
|
|||
|
|
// will complain that the type is ambiguous.
|
|||
|
|
elaboratedType(
|
|||
|
|
hasDeclaration(recordDecl().bind("struct_decl"))
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
).bind("typedef_decl");
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
A few structs in the driver share the same definition, so they are defined via
|
|||
|
|
`typedef`s to each other. These structs will not get matched by the expression
|
|||
|
|
above; instead, the tool should check that the typedefDecl is mapped to another
|
|||
|
|
`typedefDecl` rather than a `recordDecl`, like so:
|
|||
|
|
|
|||
|
|
```c++
|
|||
|
|
// Matches definitions like
|
|||
|
|
// typedef NV906F_CTRL_GET_CLASS_ENGINEID_PARAMS NVC36F_CTRL_GET_CLASS_ENGINEID_PARAMS;
|
|||
|
|
typedefDecl(
|
|||
|
|
allOf(
|
|||
|
|
hasName(struct_name),
|
|||
|
|
// Match and bind to the struct declaration.
|
|||
|
|
hasType(
|
|||
|
|
// Need to specify elaboratedType, otherwise hasType
|
|||
|
|
// will complain that the type is ambiguous.
|
|||
|
|
elaboratedType(
|
|||
|
|
hasDeclaration(typedefDecl())
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
).bind("typedef_decl");
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
These cases can be recorded as type aliases in the JSON output, described in
|
|||
|
|
more detail below.
|
|||
|
|
|
|||
|
|
Running this matcher will provide a binding to a `clang::RecordDecl` node
|
|||
|
|
corresponding to the struct definition. From here, we can iterate through the
|
|||
|
|
fields and get their name and type using `clang::FieldDecl.getNameAsString()`
|
|||
|
|
and `clang::FieldDecl->getType().getAsString()`.
|
|||
|
|
|
|||
|
|
One edge case is if the field type is an anonymous struct or union, like so:
|
|||
|
|
|
|||
|
|
```c++
|
|||
|
|
typedef struct IOCTL_WITH_UNION {
|
|||
|
|
int foo;
|
|||
|
|
union {
|
|||
|
|
int bar;
|
|||
|
|
int baz;
|
|||
|
|
} data;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Trying to get the type name directly will yield an auto generated name that
|
|||
|
|
includes the absolute file path, which is not easy to compare. Instead, the tool
|
|||
|
|
should check if a type is anonymous using `clang::Type.hasUnnamedOrLocalType`,
|
|||
|
|
and create a standardized name if not. The standardized name can be of the form
|
|||
|
|
`PARENT_RECORD::FIELD_t`; for example, `IOCTL_WITH_UNION::data_t` for the
|
|||
|
|
example above.
|
|||
|
|
|
|||
|
|
The Clang tool should also recurse into any nested structs. Since it already has
|
|||
|
|
the `clang::QualType` of each field, there are two possible cases to consider:
|
|||
|
|
|
|||
|
|
- If the type is an array, it should recurse on the array element type.
|
|||
|
|
- If the type is a struct, it can recurse on the type's `clang::RecordDecl`
|
|||
|
|
node.
|
|||
|
|
|
|||
|
|
Along the way, the tool can also record the true type of any other field types
|
|||
|
|
it find using `clang::QualType.getCanonicalType()`, in case these simple types
|
|||
|
|
ever change. For example, the tool might record that `NvHandle` is an `unsigned
|
|||
|
|
int`
|
|||
|
|
|
|||
|
|
Finally, the Go side needs some way to interface with the C++ Clang side. To
|
|||
|
|
make things simple, the inputs and outputs can be encoded in JSON. Overall,
|
|||
|
|
interfacing with the parser would go something like this:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
./driver_ast_parser --structs=structs.json source_file.cc
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Input:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"structs": ["STRUCT", "NAMES", "HERE", ...]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Output:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
// Named records since this captures both structs and unions found
|
|||
|
|
"records": {
|
|||
|
|
"STRUCT_NAME": {
|
|||
|
|
"fields": [
|
|||
|
|
{"name": "field1", "type": "int"},
|
|||
|
|
{"name": "field2", "type": "NvHandle"}
|
|||
|
|
],
|
|||
|
|
"source": "/path/to/source/file.cc:line_number"
|
|||
|
|
},
|
|||
|
|
...
|
|||
|
|
},
|
|||
|
|
// All the typedefs found
|
|||
|
|
"aliases": {
|
|||
|
|
"NvHandle": "unsigned int"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Remaining details
|
|||
|
|
|
|||
|
|
Beyond the nvproxy changes and C++ Clang parser, there are a few other details
|
|||
|
|
to work out.
|
|||
|
|
|
|||
|
|
The first is actually getting the driver source code locally for Clang to parse
|
|||
|
|
through. This can be done by cloning from the NVIDIA driver's GitHub repo:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git clone -b $VERSION --depth 1 https://github.com/NVIDIA/open-gpu-kernel-modules.git $SAVE_PATH
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Next, the parser needs some source file to analyze and parse through. As
|
|||
|
|
mentioned above, the easiest way to make this would be to create a single C++
|
|||
|
|
file that `#include`s every relevant driver header file with struct definitions.
|
|||
|
|
Finding these relevant header files does require hard-coding some paths;
|
|||
|
|
however, the driver file structure seems very stable for now. Currently, the
|
|||
|
|
list of header files is:
|
|||
|
|
|
|||
|
|
- Frontend:
|
|||
|
|
- `src/common/sdk/nvidia/inc/nvos.h`
|
|||
|
|
- `src/nvidia/arch/nvalloc/unix/include/nv-ioctl.h`
|
|||
|
|
- `src/nvidia/arch/nvalloc/unix/include/nv-unix-nvos-params-wrappers.h`
|
|||
|
|
- UVM:
|
|||
|
|
- `kernel-open/nvidia-uvm/uvm_ioctl.h`
|
|||
|
|
- `kernel-open/nvidia-uvm/uvm_linux_ioctl.h`
|
|||
|
|
- Control commands:
|
|||
|
|
- `src/common/sdk/nvidia/inc/ctrl/*.h`
|
|||
|
|
- `src/common/sdk/nvidia/inc/ctrl/*/*.h`
|
|||
|
|
- Allocation classes:
|
|||
|
|
- `src/common/sdk/nvidia/inc/class/*.h`
|
|||
|
|
|
|||
|
|
These header files also `#include` from other header files. The include paths
|
|||
|
|
for these files are as follows:
|
|||
|
|
|
|||
|
|
- Non-UVM:
|
|||
|
|
- `src/common/sdk/nvidia/inc`
|
|||
|
|
- `src/common/shared/inc`
|
|||
|
|
- `src/nvidia/arch/nvalloc/unix/include`
|
|||
|
|
- UVM:
|
|||
|
|
- `kernel-open/common/inc`
|
|||
|
|
|
|||
|
|
Unfortunately, there are many duplicate definitions between non-UVM and UVM
|
|||
|
|
files. This means that the C++ parser should be run **twice** per driver
|
|||
|
|
version, for the non-UVM and UVM sources respectively.
|
|||
|
|
|
|||
|
|
To let Clang know about these include paths, a `compile_commands.json` file is
|
|||
|
|
needed. The format of this file is documented
|
|||
|
|
[here](https://clang.llvm.org/docs/JSONCompilationDatabase.html), but for the
|
|||
|
|
use case of this tool, the structure will always look as follows:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
[
|
|||
|
|
{ "directory": "source/file/directory",
|
|||
|
|
"arguments": ["clang", "-I", "include/path/1", "-I", "include/path/2", ..., "non_uvm_source_file.cc"],
|
|||
|
|
"file": "non_uvm_source_file.cc"
|
|||
|
|
},
|
|||
|
|
// repeated for UVM source file
|
|||
|
|
]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Clang **requires** that the file is called `compile_commands.json`, and it
|
|||
|
|
assumes that it exists in the same directory as the file being parsed. As such,
|
|||
|
|
the differ will likely need to create a temporary directory when running, with
|
|||
|
|
the following format:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
temp_dir
|
|||
|
|
\ driver_source_dir
|
|||
|
|
\ compile_commands.json
|
|||
|
|
\ non_uvm_source_file.cc
|
|||
|
|
\ uvm_source_file.cc
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Altogether, the differ will behave as follows:
|
|||
|
|
|
|||
|
|
1. Get the versions A and B to be diffed.
|
|||
|
|
- Initially, these can just be passed in via command line arguments. In
|
|||
|
|
the future, the tool can just take in the new version B, and
|
|||
|
|
automatically figure out the latest version A that nvproxy supports.
|
|||
|
|
2. Query nvproxy for the list of structs it depends on for version A.
|
|||
|
|
3. Save the list of structs to a temporary JSON file.
|
|||
|
|
4. For each version:
|
|||
|
|
1. Create a temporary directory.
|
|||
|
|
2. Clone the git repo for the current version.
|
|||
|
|
3. Match the list of header file paths to create `non_uvm_source_file.cc`
|
|||
|
|
and `uvm_source_file.cc`.
|
|||
|
|
4. Create `compile_commands.json`.
|
|||
|
|
5. Run the C++ parser on both source files, referring to the list of
|
|||
|
|
structs saved above.
|
|||
|
|
6. Combine outputs from two parser runs.
|
|||
|
|
5. Compare the combined outputs of each version, reporting any differences
|
|||
|
|
found.
|
|||
|
|
|
|||
|
|
### Tests
|
|||
|
|
|
|||
|
|
Do you love tests? Well luckily for you, there are a few tests that should be
|
|||
|
|
built around this diffing tool.
|
|||
|
|
|
|||
|
|
First, a few continuous tests should be made to ensure the list of struct names
|
|||
|
|
is kept up to date. For every version covered by nvproxy’s ABI tree, one test
|
|||
|
|
can check whether there are any supported ioctls that are missing in
|
|||
|
|
`driverStructNames`, and another can run the parser to verify that every struct
|
|||
|
|
name reported in `driverStructNames` actually exists in the driver source code.
|
|||
|
|
|
|||
|
|
There should also be a continuous test that uses this tool to verify that
|
|||
|
|
nvproxy is correct. Rather than trying to use the differ, however, it might be
|
|||
|
|
easier to just use the C++ Clang parser and verify individual versions of the
|
|||
|
|
ABI. This test should take the `driverStructNames` for a given version, find the
|
|||
|
|
corresponding driver struct definitions, and then match it against the nvproxy
|
|||
|
|
equivalent struct.
|
|||
|
|
|
|||
|
|
This would require augmenting the `driverABI` mapping to also return struct
|
|||
|
|
instances, which can be read using Go’s `reflect` library. Specifically, instead
|
|||
|
|
of mapping ioctls to `[]strings`, they can be mapped to slices of strings and
|
|||
|
|
struct instances, like so:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
type DriverStruct struct {
|
|||
|
|
Name string
|
|||
|
|
Instance any
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type driverStructNames struct {
|
|||
|
|
frontendNames map[uint32][]DriverStruct
|
|||
|
|
uvmNames map[uint32][]DriverStruct
|
|||
|
|
controlNames map[uint32][]DriverStruct
|
|||
|
|
allocationNames map[nvgpu.ClassID][]DriverStruct
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
This allows for comparisons of struct definitions within nvproxy and the NVIDIA
|
|||
|
|
driver.
|
|||
|
|
|
|||
|
|
When verifying a struct, there are a few cases that can happen. The first case
|
|||
|
|
is when nvproxy treats an ioctl as simple (`DriverStruct.Instance == nil`). The
|
|||
|
|
test should look for a few signs in the driver definition, to verify that the
|
|||
|
|
struct is actually simple:
|
|||
|
|
|
|||
|
|
- If a field is `NvP64`, the struct is not simple.
|
|||
|
|
- If a field name ends in `"fd"`, the struct is not simple.
|
|||
|
|
|
|||
|
|
Another case is when nvproxy defines a struct for a parameter, but the Nvidia
|
|||
|
|
driver uses a simple type alias. `NvHandle` seems to be the only example of
|
|||
|
|
this:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// nvproxy definition
|
|||
|
|
type Handle struct {
|
|||
|
|
Val uint32 `nvproxy:"NvHandle"`
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```c++
|
|||
|
|
// Driver definition
|
|||
|
|
typedef NvU32 NvHandle;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
To verify this, the test can compare the sizes of the two types and ensure they
|
|||
|
|
remain identical.
|
|||
|
|
|
|||
|
|
The last case is when both nvproxy and the driver have struct definitions. When
|
|||
|
|
thinking about how this can be done, there are a few complications to keep in
|
|||
|
|
mind:
|
|||
|
|
|
|||
|
|
- Sometimes nvproxy flattens structs or unions. For example:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// nvproxy definition
|
|||
|
|
type IOCTL_WITH_NESTED_STRUCT struct {
|
|||
|
|
int foo;
|
|||
|
|
int bar;
|
|||
|
|
int baz;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```c++
|
|||
|
|
// Driver definition
|
|||
|
|
typedef struct {
|
|||
|
|
int foo;
|
|||
|
|
|
|||
|
|
struct {
|
|||
|
|
int bar;
|
|||
|
|
int baz;
|
|||
|
|
} data;
|
|||
|
|
} IOCTL_WITH_NESTED_STRUCT;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- Some unions are simply represented by `[n]byte` fields.
|
|||
|
|
|
|||
|
|
- Some nvproxy structs use struct embedding, which should be accounted for
|
|||
|
|
when looking through the fields using `reflect`.
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
type NV_MEMORY_ALLOCATION_PARAMS_V545 struct {
|
|||
|
|
NV_MEMORY_ALLOCATION_PARAMS `nvproxy:"NV_MEMORY_ALLOCATION_PARAMS"`
|
|||
|
|
NumaNode int32
|
|||
|
|
_ uint32
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- nvproxy structs can have additional fields added for padding.
|
|||
|
|
|
|||
|
|
To alleviate the problem of nested or flattened structs, all struct definitions
|
|||
|
|
can be pre-flatten before comparing them. This will yield an array of fields for
|
|||
|
|
both sides. For example, this definition
|
|||
|
|
|
|||
|
|
```c++
|
|||
|
|
typedef struct {
|
|||
|
|
int a1;
|
|||
|
|
int a2;
|
|||
|
|
IOCTL_B b;
|
|||
|
|
} IOCTL_A;
|
|||
|
|
|
|||
|
|
typedef struct {
|
|||
|
|
bool b1;
|
|||
|
|
bool b2;
|
|||
|
|
IOCLT_C c;
|
|||
|
|
bool b3;
|
|||
|
|
} IOTCL_B;
|
|||
|
|
|
|||
|
|
typedef struct {
|
|||
|
|
unsigned int c;
|
|||
|
|
} IOCTL_C;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
would be flattened into
|
|||
|
|
|
|||
|
|
```c++
|
|||
|
|
[
|
|||
|
|
int a1,
|
|||
|
|
int a2,
|
|||
|
|
bool b1,
|
|||
|
|
bool b2,
|
|||
|
|
unsigned int c,
|
|||
|
|
bool b3,
|
|||
|
|
]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Next, fields that **have the same offset** should be compared. Due to padding
|
|||
|
|
and union types, multiple nvproxy fields may correspond to a single driver
|
|||
|
|
field; however, as long as each driver field has a corresponding nvproxy field
|
|||
|
|
at the same offset, the extraneous fields do not matter. The following
|
|||
|
|
pseudo-code accomplishes all of this:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
doStructsMatch(nvproxyType, driverType) -> bool
|
|||
|
|
if nvproxyType.Size != driverType.Size:
|
|||
|
|
return false
|
|||
|
|
|
|||
|
|
nvproxyFields = Flatten(nvproxyType)
|
|||
|
|
driverFields = Flatten(driverType)
|
|||
|
|
|
|||
|
|
for each ith field in driverFields:
|
|||
|
|
find the jth field in nvproxyFields with the same offset
|
|||
|
|
if such a field doesn't exist:
|
|||
|
|
return false
|
|||
|
|
|
|||
|
|
if !doTypesMatch(nvproxyFields[j].Type, driverFields[i].Type):
|
|||
|
|
return false
|
|||
|
|
return true
|
|||
|
|
|
|||
|
|
doTypesMatch(nvproxyType, driverType) -> bool
|
|||
|
|
if driverType is an array:
|
|||
|
|
if nvproxyType is not an array of the same length:
|
|||
|
|
return false
|
|||
|
|
recurse on the base type of each array
|
|||
|
|
|
|||
|
|
// These are special types that nvproxy has type definitions for
|
|||
|
|
Check the following mappings from driverType -> nvproxyType:
|
|||
|
|
NvHandle -> Handle
|
|||
|
|
NvP64 -> P64
|
|||
|
|
NvProcessorUuid -> NvUUID
|
|||
|
|
|
|||
|
|
// E.g. NvU32 aliases unsigned int
|
|||
|
|
if driverType has an alias:
|
|||
|
|
driverType = alias
|
|||
|
|
Check the following mappings from driverType -> nvproxyType:
|
|||
|
|
char -> byte
|
|||
|
|
unsigned char -> uint8
|
|||
|
|
short -> int16
|
|||
|
|
unsigned short -> uint16
|
|||
|
|
int -> int32
|
|||
|
|
unsigned int -> uint32
|
|||
|
|
long long -> int64
|
|||
|
|
unsigned long long -> uint64
|
|||
|
|
enum _ -> uint32
|
|||
|
|
union -> [n]byte
|
|||
|
|
struct -> doStructsMatch(nvproxyType, driverType)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
This all requires some changes on the C++ parser side as well. Namely, it should
|
|||
|
|
report sizes for `records` and `aliases`, whether a record is a union type, and
|
|||
|
|
offsets for each record field. This can be done with
|
|||
|
|
`clang::ASTContext.getTypeInfo`, `clang::TagDecl.isUnion`, and
|
|||
|
|
`clang::ASTContext.getFieldOffset` respectively.
|
|||
|
|
|
|||
|
|
## Future Work
|
|||
|
|
|
|||
|
|
### Interpreting struct field names
|
|||
|
|
|
|||
|
|
Occasionally, driver structs might change not by introducing a new field, but by
|
|||
|
|
changing the purpose of an existing field. For example, a previously reserved
|
|||
|
|
integer field might now be used as a file descriptor field, meaning that nvproxy
|
|||
|
|
would need to add special handling for it. Although the differ reports changes
|
|||
|
|
in field names, it could also report any code changes it thinks are necessary.
|
|||
|
|
This could behave similarly to the verification test, which looks at simple
|
|||
|
|
clues such as `NvP64` types or fields ending in `"fd"`.
|
|||
|
|
|
|||
|
|
### Check ABI ranges for nvproxy
|
|||
|
|
|
|||
|
|
Currently, nvproxy only support specific versions of the Nvidia driver. However,
|
|||
|
|
many intermediate versions likely do not have any breaking changes, and it is
|
|||
|
|
detrimental to users if they are forced to only use some specific driver
|
|||
|
|
versions. This differ tool could be used to find ranges of ABI versions that
|
|||
|
|
have no change, and nvproxy could support any version with this range.
|
|||
|
|
|
|||
|
|
### Additional nvproxy struct tags
|
|||
|
|
|
|||
|
|
In the future, nvproxy can record additional information using the
|
|||
|
|
`nvproxy:"..."` tags. For example, any `NvP64` field could be tagged with the
|
|||
|
|
struct type that the pointer represents, allowing tests to recurse on these
|
|||
|
|
hidden dependencies.
|