-
Notifications
You must be signed in to change notification settings - Fork 5.5k
[cdac] Data Descriptor Spec #100253
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[cdac] Data Descriptor Spec #100253
Changes from 2 commits
d392600
980144a
67eee3f
019bc63
452c990
8224050
11e988c
db02e42
b40fa38
a46b9e2
7896c0a
27f9892
b2dba2c
ddb0a4b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,310 @@ | ||
| # Data Descriptors | ||
|
|
||
| The [data contract](datacontracts_design.md) specification for .NET depends on each target .NET | ||
| runtime describing a subset of its platform- and build-specific data structures to diagnostic | ||
| tooling. The information is given meaning by algorithmic contracts that describe how the low-level | ||
| layout of the memory of a .NET process corresponds to high-level abstract data structures that | ||
| represent the conceptual state of a .NET process. | ||
|
|
||
| In this document we give a logical description of a data descriptor together with two physical | ||
| manifestations. | ||
|
|
||
| The first physical format is used to publish well-known data descriptors in the `dotnet/runtime` | ||
| repository. It is supposed to be machine- and human-readable. This format is not meant to be | ||
| particularly concise and may be used for visualization, diagnostics, etc. Typically data | ||
| descriptors in this form may be written by hand or with the aid of tooling. | ||
|
|
||
| The second physical format is used to embed a data descriptor blob within a particularly instance of | ||
| a target runtime. It is meant to be machine-readable while minimizing the total space needed to | ||
| store it. It is primarily meant to be read and written by tooling. | ||
|
|
||
| ## Logical descriptor | ||
|
|
||
| Each logical descriptor exists within an implied /target architecture/ consisiting of: | ||
| * target architecture endianness (little endian or big endian) | ||
| * target architecture pointer size (4 bytes or 8 bytes) | ||
|
|
||
| The following /primitive types/ are assumed: int8, uint8, int16, uint16, int32, uint32, int64, | ||
| uint64, nint, nuint, pointer, (UTF-8) string. The multi-byte types are in the target architecture | ||
| endianness. The types `nint`, `nuint` and `pointer` have target architecture pointer size. | ||
|
|
||
| The data descriptor consists of: | ||
| * the data descriptor specification version | ||
| * a collection of type structure descriptors | ||
| * a collection of global value descriptors. | ||
|
|
||
| ## Data descriptor specification version | ||
|
|
||
| This is the version of the physical data descriptor. | ||
|
|
||
| ## Types | ||
|
|
||
| The types (both primitive types and structures described by structure descriptors) are classified as | ||
| having either determinate or indeterminate size. Determinate sizes may be used for pointer | ||
| arithmetic. Types with indeterminate size may not be. Note that some sizes may be determinate, but | ||
|
lambdageek marked this conversation as resolved.
Outdated
|
||
| /target specific/. For example pointer types have a fixed size that varies by architecture. | ||
|
|
||
| The following primitive types have indeterminate size: `string`. | ||
|
|
||
| ## Structure descriptors | ||
|
|
||
| Each structure descriptor consists of: | ||
| * a name | ||
| * an optional size in bytes | ||
| * a collection of field descriptors | ||
|
|
||
| If the size is not given, the type has indeterminate size. The size may also be given explicitly as | ||
| "indeterminate" to emphasize that the type has indeterminate size. | ||
|
|
||
| The collection of field descriptors may be empty. In that case the type is opaque. The primitive | ||
| types may be thought of as opaque (for example: on ARM64 `nuint` is an opaque 8 byte type, `int64` | ||
| is another opaque 8 byte type. `string` is an opaque type of indeterminate size). | ||
|
|
||
| Type names must be globally unique within a single logical descriptor. | ||
|
lambdageek marked this conversation as resolved.
|
||
|
|
||
| ### Field descriptors | ||
|
|
||
| Each field descriptor consists of: | ||
| * a name | ||
| * an offset in bytes from the beginning of the struct | ||
| * a type or the special type `variable` | ||
|
noahfalk marked this conversation as resolved.
Outdated
lambdageek marked this conversation as resolved.
Outdated
|
||
|
|
||
| The name of a field descriptor must be unique within the definition of a structure. | ||
|
|
||
| The offset may be negative. | ||
|
lambdageek marked this conversation as resolved.
Outdated
|
||
|
|
||
| Two or more fields may have the same offsets or imply that the underlying fields overlap. The field | ||
| offsets need not be aligned using any sort of target-specific alignment rules. | ||
|
|
||
| Each field's type may refer to one of the primitive types or to any other type defined in the logical descriptor. | ||
|
|
||
| If the field's type is `variable` it represents a byte-array of indeterminate size starting at the given offset. | ||
|
|
||
| If a structure descriptor contains at least one field of indeterminate size, the whole structure | ||
| must have indeterminate size. Tooling is not required to, but may, signal a warning if a descriptor | ||
| has a determinate size and contains indeterminate size fields. | ||
|
|
||
| It is expected that tooling will signal a warning if a field specifies a type that does not appear | ||
| in the logical descriptor. | ||
|
|
||
| ## Global value descriptors | ||
|
|
||
| Each global value descriptor consists of: | ||
| * a name | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My guess is that ordinals would be a bit simpler as well as being more compact, but if it fits in the size targets I'm fine with names. I wouldn't be surprised if a dense ordinal-based encoding without any baseline values also fits within the perf size goals. If that meant we didn't need any custom build tooling, JSON format, or baseline offset tables that seems like a nice simplification. The part that remains an unknown for me on that route is how we deal with C# globals/types in NativeAOT assuming that we would have some. So far all the DebugHeader work for NativeAOT has never needed to encode info for managed type or global and I don't know what capabilities we have to work with in the NativeAOT toolchain.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It sounds like my suggestion is not the direction you are choosing to go. If you want to discuss it more I'm happy to, but I also don't want to be a distraction when you've decided on another approach already. I believe the current approach with JSON, baselines, and custom build tools can work even if it feels more complex than needed to me. Feel free to mark this resolved. |
||
| * a type | ||
| * a value | ||
|
|
||
| The name of each global value must be unique within the logical descriptor. | ||
|
|
||
| The type must be one of the determinate-size primitive types. | ||
|
|
||
| The value must be a integral constant within the range of its type. Signed values are two's | ||
| complement. Pointer values need not be aligned and need not point to addressable target memory. | ||
|
|
||
|
|
||
| ## Physical descriptors | ||
|
|
||
| The physical descriptors are meant to describe /subsets/ of a logical descriptor and to compose. | ||
| Each physical descriptor can name an ordered sequence of zero or more "baseline" descriptor which is then | ||
| considered to comprise a piece of the overall logical descriptor. | ||
|
lambdageek marked this conversation as resolved.
Outdated
|
||
|
|
||
| Starting from a single physical descriptor, the "baseline" relationship forms a directed graph. It | ||
| is an error for the graph to contain a cycle. The baseline relationship may form a DAG (that is: two | ||
| or more nodes may refer to the same baseline). | ||
|
|
||
| When constructing the logical descriptor, the DAG is traversed in a post-order traversal with each | ||
| node visited at most once with baselines of a particular node visited from first to last. | ||
|
|
||
| To form the logical descriptor the types are added in traversal order with later appearances | ||
| augmenting earlier ones (fields are added or modified, sizes and offsets are overwritten). The | ||
| global values are added in traversal order with later appearances overwriting previous ones. | ||
|
|
||
| Rationale: if a baseline is included more than once, only the first inclusion counts. If a type | ||
| appears in multiple physical descriptors, the later appearances may add more fields or change the | ||
| offsets or definite/indefinite sizes of prior definitions. If a value appears multipel times, later | ||
| definitions take precedence. | ||
|
|
||
| **FIXME** do we really want a DAG? Are we ok with a linked list? | ||
|
lambdageek marked this conversation as resolved.
Outdated
|
||
|
|
||
| ## Physical JSON descriptor | ||
|
|
||
| ### Version | ||
|
|
||
| This is version 0 of the physical descriptor | ||
|
|
||
| ### Summary | ||
|
|
||
| A data descriptor may be stored in the "JSON with comments" format. | ||
|
|
||
| The toplevel dictionary will contain: | ||
|
|
||
| * `"version": 0` | ||
| * `"baseline": "FREEFORM STRING"` or `baseline: ["FREEFORM STRING"", ...]` | ||
| * `"types": TYPE_ARRAY` see below | ||
| * `"globals": VALUE_ARRAY` see below | ||
|
|
||
| ### Types | ||
|
|
||
| The types will be in an array, with each type described by a dictionary containining keys: | ||
|
|
||
| * `"name": "type name"` the name of each type | ||
|
lambdageek marked this conversation as resolved.
|
||
| * optional `"size": int | "indeterminate"` if omitted the size is indeterminate | ||
| * optional `"fields": FIELD_ARRAY` if omitted same as a field array of length zero | ||
|
|
||
| Each `FIELD_ARRAY` is an array of dictionaries each containing keys: | ||
|
|
||
| * `"name": "field name"` the name of each field | ||
| * `"type": "type name"` the name of a primitive type or another type defined in the same /logical/ descriptor | ||
| * optional `"offset": int | "unknown"` the offset of the field or "unknown". If omitted, same as "unknown". | ||
|
|
||
| Note that the logical descriptor does not contain "unknown" offsets. | ||
|
|
||
| Rationale: "unknown" offsets may be used to document in the physical JSON descriptor that another | ||
| physical descriptor in the "baseline" graph is expected to provide the offset of the field. | ||
|
|
||
| ### Global values | ||
|
|
||
| The global values will be in an array, with each value described by a dictionary containing keys: | ||
|
|
||
| * `"name": "global value name"` the name of the global value | ||
| * `"type": "type name"` the type of the global value | ||
| * optional `"value": VALUE | "unknown"` the value of the global value or "unknown". If omitted, same as "unknown". | ||
|
|
||
| Note that the logical descriptor does not contain "unknown" values. | ||
|
|
||
| The `VALUE` may be a JSON numeric constant integer or a string containing a signed or unsigned | ||
| decimal or hex (with prefix `0x` or `0X`) integer constant. The constant must be within the range | ||
| of the type of the global value. | ||
|
|
||
| For pointer and nuint globals, the value may be assumed to fit in a 64-bit unsigned integer. For | ||
| nint globals, the value may be assumed to fit in a 64-bit signed integer. | ||
|
|
||
| If the value is specified as "unknown" another physical descriptor in the "baseline" graph is | ||
| expected to provide the value. | ||
|
|
||
| ## Physical binary blob descriptor | ||
|
|
||
| ### Version | ||
|
|
||
| This is version 0 of the physical binary blob format. | ||
|
|
||
| ### Summary | ||
|
|
||
| The binary blob format for a physical descriptor is expected to be stored in the memory space of a | ||
| target .NET runtime and is thus likely to be the final descriptor in the "baseline" graph. | ||
|
|
||
| It is likely that the physical descriptor will be a part of a build-time constant in the disk image | ||
| of a .NET runtime, and the format is designed to be compact and specifiable as a (likely | ||
| machine-generated) compile-time constant in a suitable source language. | ||
|
|
||
| The data descriptor forms one part of an overall physical data contract descriptor in a targret .NET | ||
| runtime and as such this format does not specify a "magic number", or "well known symbol", or | ||
| another means of identifying the blob within a target process. Additionally the version of the | ||
| binary blob data descriptor is expected to be stored within the larger enclosing data contract | ||
| descriptor and is not included here. | ||
|
|
||
| ### Blob | ||
|
|
||
| Multi-byte values are in the target platform endianness. | ||
|
|
||
| The format is: | ||
|
|
||
| ```c | ||
| struct BinaryBlobDataDescriptor | ||
| { | ||
| struct Directory { | ||
| uint32_t TypesStart; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When would these Start values not be 0?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this comes from teh requirement that we would like to be able to read binary descriptors directly out of object files without understanding the object file format. That means that if the object file is produced by a C compiler, we want to be able to ignore any padding or alignment that the C compiler adds. So because we have fixed-size arrays of structs as part of
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does that mean this field is the relative offset from the start of the BinaryBlobDescriptor to the start of the Types array and we'd never expect a reader to know that BinaryBlobDescriptor has a Types field? If I followed you correctly I'd be tempted to omit all of those array fields from the struct definition or add a comment saying those fields are implementation details that a reader shouldn't rely upon.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
+1. We do not need to spec the intermediate artifacts used in the process of building the final descriptor. They can be just our internal implementation detail.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jkotas ok, I'm gonna take one more pass at simplifying things:
The build tooling (I'm thinking of it as
At diagnostic time:
|
||
| uint32_t FieldPoolStart; | ||
|
|
||
| uint32_t GlobalValuesStart; | ||
| uint32_t NamesStart; | ||
|
|
||
| uint32_t TypeCount; | ||
| uint32_t FieldPoolCount; | ||
|
|
||
| uint32_t NamesPoolCount; | ||
| uint32_t Reserved0; | ||
|
|
||
| uint16_t TypeSpecSize; | ||
|
noahfalk marked this conversation as resolved.
Outdated
|
||
| uint16_t FieldSpecSize; | ||
|
|
||
| uint16_t GlobalSpecSize; | ||
| uint16_t Reserved1; | ||
| } | ||
| uint32_t BaselineName; | ||
| TypeSpec[TypeCount] Types; | ||
| FieldSpec[FieldPoolCount] FieldPool; | ||
| GlobalSpec[GlobalsCount] GlobalValues; | ||
| uint8_t[NamesPoolCount] NamesPool; | ||
| }; | ||
|
|
||
| struct TypeSpec | ||
| { | ||
| uint32_t Name; | ||
| uint32_t Fields; | ||
| uint16_t Size; | ||
| }; | ||
|
|
||
| struct FieldSpec | ||
| { | ||
| uint32_t Name; | ||
| uint16_t FieldOffset; | ||
| uint16_t Reserved; | ||
| }; | ||
|
|
||
| struct GlobalSpec | ||
| { | ||
| uint32_t Name; | ||
| uint32_t Reserved; | ||
| uint64_t Value; | ||
| }; | ||
| ``` | ||
|
|
||
| Rationale: the blob should be producable by including a specially formatted C header file multiple | ||
|
lambdageek marked this conversation as resolved.
Outdated
|
||
| times with redefinitions of some macro names appearing in its content. Additionally the blob avoids | ||
| pointers so that it may be read off from the on-disk representation of an object file. | ||
|
|
||
| The blob begins with a directory that gives the relative offsets of the `Types`, `FieldPool`, | ||
| `GlobalValues` and `Names` fields of the blob. The number of elements of each of the arrays is | ||
| next. This is followed by the sizes of the `TypeSpec`, `FieldSpec` and `GlobalSpec` structs. | ||
|
|
||
| Rationale: If a `BinaryBlobDataDescriptor` is created via C macros, we want to embed the `offsetof` | ||
| and `sizeof` of the components of the blob into the blob itself without having to account for any | ||
| padding that the C compiler may introduce to enfore alignment. | ||
|
|
||
| The baseline is specified as an offset into the names pool. | ||
|
|
||
| The types are given as an array of `TypeSpec` elements. Each one contains an offset into the | ||
| `NamesPool` giving the name of the type, An offset into the fields pool indicating the first | ||
| specified field of the type, and the size of the type in bytes or 0 if it is indeterminate. | ||
|
|
||
| The fields pool is given as a sequence of `FieldSpec` elements. The fields for each type are given | ||
| in a contiguious subsequence and are terminated by a marker `FieldSpec` with a `Name` offset of 0. | ||
| (Thus if a type has an empty sequence of fields it just points to a marker field spec directly.) | ||
| For each field there is a name that gives an offset in the name pool and an offset indicating the | ||
| field's offset. The field type is not given. | ||
|
|
||
| Rationale: it is expected that the types of the fields were provided by a "baseline" data descriptor. | ||
|
lambdageek marked this conversation as resolved.
Outdated
|
||
|
|
||
| The globals are gives as a sequence of `GlobalSpec` elements. Each global has a name and a value. | ||
| The types of the globals are not given. | ||
|
|
||
| Rationale: it is expected that the types of the global values were provided by a "baseline" data descriptor. | ||
|
lambdageek marked this conversation as resolved.
Outdated
|
||
|
|
||
| The `NamesPool` is a single sequence of utf-8 bytes comprising the concatenation of all the type | ||
| field and global names including a terminating nul byte for each name. The same name may occur | ||
| multiple times. The names will be referenced by multiple type or multiple fields. (That is, a | ||
| clever blob emitter may pool strings). The first name in the name pool is the empty string (with | ||
| its nul byte). | ||
|
|
||
| Rationale: we want to reserve the offset 0 as a marker. | ||
|
|
||
| Names are referenced by giving their offset from the beginning of the `NamesPool`. Each name | ||
| extends until the first nul byte encountered at or past the beginning of the name. | ||
|
|
||
|
|
||
| ## Example | ||
|
|
||
| And example C header describing some data types is given in [sample.data.h](./sample.data.h). And | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any thoughts on what this might look like for NativeAOT data structures defined in C#?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the resulting .o file it will look the same. it can be produced by other means - it doesn't need to be produced by C marcros. (In fact even for CoreCLR it doesn't need to be produced by macros. It can be created with C++
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I wasn't worried that it would be constrained to macros :) I just have no understanding of what alternative method we would use to generate it instead and I'm searching for info that helps me fill in that gap.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I have no idea. Perhaps a new phase for ILCompiler. or possibly some source generator that runs over NativeAOT's runtime source code and produces a C# blittable struct definition that follows the format we expect
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I would expect a variant of |
||
| example series of C macro preprocessor definitions that produces a constant blob `Blob` is given in | ||
| [sample.blob.c](./sample.blob.c) | ||
Uh oh!
There was an error while loading. Please reload this page.