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.
|