Skip to content

Commit

Permalink
added description of how we validate inline call data
Browse files Browse the repository at this point in the history
  • Loading branch information
brigadier-general authored Jun 6, 2024
1 parent 41e39ab commit 911a044
Showing 1 changed file with 49 additions and 6 deletions.
55 changes: 49 additions & 6 deletions doc/inlinedFunctions.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,55 @@ The documentation implies that each function that contains inlined functions wil
5. Save `funcData.funcdata[FUNCDATA_InlTree]` -- this is the inline tree offset for `f`
6. Get `go:func.*` via `moduledata`. (there are other ways but this is the least complicated)
7. Adjust `go:func.*` from absolute address to file offset by subtracting the preferred base address (in file header). `go:func.* -= baseAddress`
9. Go to inline tree. InlineTreeAddress = `go:func.*` + `funcData.funcdata[FUNCDATA_InlTree]`.
9. Go to inline tree. InlineTreeAddress = `go:func.*` + `funcData.funcdata[FUNCDATA_InlTree]`. This is an offset relative to the start of the binary file because we adjusted `go:func.*` in step 7 above.

*NOTE: the inline tree and `go:func.*` addresses may be earlier in the binary than `pclntab`*
Therefore whatever component resolves inline functions MUST have access to the full file.

## Resolving a single runtime__inlinedCall
## Validating inline tree entries

We iterate from the InlineTreeAddress, grab enough bytes to fill a single `runtime__inlinedCall` instance. Validate its fields. If any validation check fails or there are not enough bytes to fill the struct, assume that we have reached the end of the tree. Return results.

```
Start at InlineTreeAddress.
While there are at least sizeof(runtime__inlinedCall) bytes not yet checked:
Get sizeof(runtime__inlinedCall) bytes as potentialCall
Check potentialCall.funcID
- funcID must be 0 (i.e. "normal")
- the subsequent padding bytes must also be 0 (number of pad bytes depends on Go version)
- if these fail, break
Check potentialCall.parentPc
- get parentFunction.Entry (aka start offset) (we have this data in the funcData used to locate the inline tree)
- get parentFunction.End (aka end offset)
- potentialCall.parentPc + parentFunction.Entry must be less than parentFunction.End
- if parentPc falls beyond the end of parentFunction, break
Check potentialCall.name (the field name varies between Go versions)
- get pcHeader.funcNameOffset
- get size in bytes of function name table
- pcHeader.funcNameOffset + potentialCall.name must fall within bounds of the function name table
- [NOT IMPLEMENTED] first char must be ASCII, previous char should be 0 (null-terminator)
- if name offset is invalid, break
Save data from the runtime__inlinedCall struct into a version-agnostic InlinedCall object
Add the new InlinedCall object to array of found InlinedCall objects
Move forward by sizeof(runtime_inlinedCall)
Return array of InlinedCall objects
```

## Known issues

### Not implemented yet for Go v1.11-1.18

## Known issues
### Only tested on ELF format

### Saving inlined function names

Currently we manually calculate the size of the string to creat the slice. There's gotta be a better way to save the string and probably some extra validation we could to make sure that the func name offset points to the start of a string.

### Only processing inlined calls where funcID==0 (normal function)

Haven't found a great description of the funcID types. We may want to include more than just normal functions. We might also find that each inline tree ends with inline info for a type. If we can find a description of the inline tree or trees section as a whole, then we might be able to use this as a pattern to separate where inline trees begin/end.

### How to calculate size of inline tree for a given `f`.

Expand All @@ -34,6 +73,10 @@ process inline call data until we hit invalid data. If two inline trees for func
with no buffer then `j`'s tree will be mistakenly included in `f`'s tree. The functions that were inlined into `j`
will be listed twice as inlined inside `f` as well as `j`.

Another heuristic to help could be checking the tree bases for all functions with inline data and stopping the inline struct iteration when we reach another function's tree base.

Haven't found any overview of an "inlined data section". Either finding one or walking through the compiler steps to build one would be useful.

### Using pcdata

We don't currently use `funcData.pcdata[PCDATA_InlineTreeIndex]`.
Expand All @@ -46,6 +89,6 @@ This info might be helpful in determining how many functions were inlined into `

## References

[pclntab structs reference](https://github.com/elastic/otel-profiling-agent/blob/main/docs/gopclntab.md)
[adding inline functions for golang debugger](https://developers.redhat.com/articles/2024/04/03/how-add-debug-support-go-stripped-binaries)
[how and why inlining with source examples](https://dave.cheney.net/2020/04/25/inlining-optimisations-in-go)
* [pclntab structs reference](https://github.com/elastic/otel-profiling-agent/blob/main/docs/gopclntab.md)
* [adding inline functions for golang debugger](https://developers.redhat.com/articles/2024/04/03/how-add-debug-support-go-stripped-binaries)
* [how and why inlining with source examples](https://dave.cheney.net/2020/04/25/inlining-optimisations-in-go)

0 comments on commit 911a044

Please sign in to comment.