Dear community
In this post I'd like to give a more in-depth view on the new
inst implementation. The goal is to provide a reference for those who wish to extend the source code with new features.
TL;DR: Download
inst_bundle.tar, install the tardists and run
/usr/local/inst/bin/inst
1. inst
=======
A quick summary on
inst(1M) for those unaware:
inst(1M) is the software installation tool used on
SGI IRIX. It provides a command line interface, though a GUI is also available in form of
swmgr(1M).
So how does inst integrate into an
SGI IRIX system? Looking at its dependencies, it becomes clear that there's a lot happening inside of inst:
Further analysis shows that it is written in
C++ with some free functions that are familiar from other well known
C libraries (
free function is a term from
C++ and refers to a
non-member function):
Code:
...
[97] | 82676544| 92|FUNC |GLOB |DEFAULT |MIPS_TEXT|getDist__12iFileRepListCGv
[98] | 82694568| 476|FUNC |GLOB |DEFAULT |MIPS_TEXT|histDelta__5iHistSGPC25iForest__pt__10_8InstableT1PC12iInstProduct
[99] | 82698300| 3336|FUNC |GLOB |DEFAULT |MIPS_TEXT|upgradeHistory__5iHistGRC7iString
[100] | 82701636| 4096|FUNC |GLOB |DEFAULT |MIPS_TEXT|downgradeHistory__5iHistGRC7iString
[101] | 82018480| 488|FUNC |GLOB |DEFAULT |MIPS_TEXT|__ls__GR7ostreamRC15iFilePrereqIter
[102] | 82026376| 52|FUNC |GLOB |DEFAULT |MIPS_TEXT|clearAllErrors__9iInstArgsGv
[103] | 82026568| 52|FUNC |GLOB |DEFAULT |MIPS_TEXT|clearMountTable__9iInstArgsGv
[104] | 82033776| 56|FUNC |GLOB |DEFAULT |MIPS_TEXT|imageThatImplies__9iInstArgsCGi
[105] | 82744728| 824|FUNC |GLOB |DEFAULT |MIPS_TEXT|computeFileSystemIndex__9iInstFileGv
[106] | 82746408| 92|FUNC |GLOB |DEFAULT |MIPS_TEXT|getMachTag__9iInstFileCGv
[107] | 82767940| 304|FUNC |GLOB |DEFAULT |MIPS_TEXT|getChildCreatedate__12iInstProductCGv
[108] | 82769560| 16|FUNC |GLOB |DEFAULT |MIPS_TEXT|setRawFlag__12iInstProductGi
[109] | 82769652| 20|FUNC |GLOB |DEFAULT |MIPS_TEXT|clearFlag__12iInstProductGi
[110] | 82770836| 20|FUNC |GLOB |DEFAULT |MIPS_TEXT|isDefault__12iInstProductCGv
[111] | 82770876| 64|FUNC |GLOB |DEFAULT |MIPS_TEXT|isFeatureStream__12iInstProductCGv
[112] | 82770940| 64|FUNC |GLOB |DEFAULT |MIPS_TEXT|isMaintStream__12iInstProductCGv
[113] | 82782840| 1164|FUNC |GLOB |DEFAULT |MIPS_TEXT|maintToBase__12iInstProductGRC6iAPathR6iAPath
[114] | 82060184| 64|FUNC |GLOB |DEFAULT |MIPS_TEXT|machNodeType
[115] | 82811412| 64|FUNC |GLOB |DEFAULT |MIPS_TEXT|is32bit__5iMachCGv
[116] | 82811792| 60|FUNC |GLOB |DEFAULT |MIPS_TEXT|unknownTags__5iMachCGRC13iMachSelector
[117] | 82062940| 1028|FUNC |GLOB |DEFAULT |MIPS_TEXT|getMachTables__GRP9Subgr_MapRP8Mach_MapRP8Arch_MapRiN24
[118] | 82841520| 52|FUNC |GLOB |DEFAULT |MIPS_TEXT|getLastDelta__11iProdActionGv
[119] | 82842132| 1064|FUNC |GLOB |DEFAULT |MIPS_TEXT|log__11iProdActionCGv
[120] | 82844536| 8|FUNC |GLOB |DEFAULT |MIPS_TEXT|setupTasks__11iProdActionGv
[121] | 82845488| 264|FUNC |GLOB |DEFAULT |MIPS_TEXT|summary__11iProdActionCGv
[122] | 82888900| 1804|FUNC |GLOB |DEFAULT |MIPS_TEXT|downgradeHistory__12iProdDescMgrGRC7iStringiT1T2PPc
[123] | 82078032| 56|FUNC |GLOB |DEFAULT |MIPS_TEXT|prodmap
...
Estimations of source lines of code are one of the best indicators to measure complexity of an application.
The following formula was applied:
SLOCs = DIS_LINES_OF_ALL_C++_METHODS * 1,25
DIS_LINES_OF_ALL_C++_METHODS: Number of disassembled lines of binary output using dis(1).
1,25: A correcting factor that compensates for divergence from the real
SLOCs. This factor includes comments and is based on experimental results from my own source code.
When applied to
inst(1M):
SLOCs = DIS_LINES_OF_ALL_C++_METHODS=465.105 * 1,25 = 581.381,25
Good.
2. Hook system
==============
With this amount of lines of code, a pure reimplementation is not feasable. A different approach is necessary.
There is one saying in business that goes like this:
EEE =
Embrace,
Extend,
Extinguish.
This is applicable in different situations, for example when you are trying to tackle on a problem that is way beyond your possibilities. You can try to adapt to it (
Embrace), modify it a bit at a time (
Extend) and finally, by repeating the previous step, completely change it (
Extinguish).
That's the approach I need here.
My
EEE is called "Hook system". A hook system is a concept used in software architecture to describe a system that consists logically of three parts:
1. A system (allowing hooks)
2. The hook that connects into the system
3. Something that is bound to the hook
On different OSs this concept is implemented with a different semantic, but logically they are all composed of these three parts (at least). As a reference for such systems, consult:
SGI IRIX - Any page about routines for registering interrupt, bus handling, error or driver
https://www.irix7.com/techpubs/007-0911-210.pdf
Windows et. al. - Any of the points listed as example uses
https://learn.microsoft.com/en-us/window...bout-hooks
Back to
inst, a hook system would be needed to hook (2.) a user defined function (3.) to the libinst.so's function (1.). The implemented semantic would result in a callback of the user defined function whenever
libinst.so's function would be called.
That way, if an extension needs to access only a certain number of functions in
libinst.so, it would just need to hook its own user defined function(s) into these
libinst.so functions.
In this scenario, there are minimum two possible techniques of implementing a hook system.
2.1. Patching
-------------
Patching means here to patch-in jump instructions into the system
libinst.so's functions at the beginning and end (into their prologue and epilogue, for example). These would jump to functions outside of
libinst.so to call the hooked functions:
The advantages of this approach are stability and flexibility.
The disadvantages are the effort needed (some functions would require unique modifications) and that it would require a whole extra copy of
libinst.so, because you would need an address space where the patched-in jump instructions can jump to the same address every time you load the library. There are some workarounds to this too, but judging only by these disadvantages it is a second best approach, if at all.
2.2. Static prelinking
----------------------
Static prelinking is a technique that allows to shadow the real
libinst.so functions with user defined ones by linking the final executable in a way that the system runtime loader,
rld, loads the user defined ones before the real ones, thereby shadowing them. Neat.
In the upper example, a normal invocation of
inst(1M) is shown. It would call into libinst.so simply using the needed function name.
In the lower example, an invocation of
inst v1.0.0 using static prelinking is shown:
inst v1.0.0 would call first into
libinstw.so (the trailing 'w' stands for wrapper) which contains, ideally, all the function names in system
libinst.so.
Libinstw.so would be linked first to
inst v1.0.0 such that the runtime linker would first load this library (and use the contained symbols for further symbol resolution, effectively shadowing libinst.so if it was subsequently loaded).
In a second step, the function in
libinstw.so, also called the wrapper function, would then delegate to the real function in the system
libinst.so. If you ask yourself how it's possible to call a shadowed function in this situation, then have a look at man
dlopen(3C).
There is a similar technique in almost any
UNIX from the 90's onwards that allows you to do just this, but using an environment variable directed at the runtime linker,
rld. See
rld(5) for more details.
The difference is the time of symbol binding. In static prelinking, symbols are bound at static link time (using
ld). The second technique binds symbols at runtime (using
rld).
NOTE: In the context I use these terms here, the difference between
static prelinking and just plain old
static linking is its intended use:
static prelinking links statically against a third library to shadow another one at runtime.
2.3. Improving wrapper functions
--------------------------------
Using
static prelinking, it is possible to implement a simple hook system, but this can be improved upon a bit.
From my personal experience using various hook systems, there is one shortcoming that immediately comes to mind and that would greatly ease some tasks (to be fair, I'm talking about relatively rare ones). It is the lack of an "epilogue function": Most hook systems just allow to set or register a hook that binds a prologue function for a specific task. But they don't allow to bind functions for the exact point in time when the task has just finished. And sometimes, this is desirable.
Since this feature can be very helpful and doesn't differ too much from the prologue function, the hook system for
inst v1.0.0 supports both of them.
In addition, it is possible to suppress the call to the real function or register your own to be called instead.
This is the kind of flexibility that makes a developer's heart go
BUM BUM.
To implement this double call before and after the system function, the wrapper function in libinstw.so would just need to call first the prologue function, then the system libinst.so function, and finally the epilogue function. In the source code, prologue functions are called pre functions, and epilogue functions are called post functions. Both are stored in the file
prepost.c (conveniently named):
All wrapper functions in
libinstw.so make a copy of all the argument registers on entry (this is done in assembly) and then call the internal
wrapper() helper function that handles the invokations of pre, system libinst.so and post functions, along with argument passing, which is detailed in point
2.6 Function arguments (see below).
Step 3 is greyed to emphasize that this step is not specific to one wrapper function (
A::get() in this illustration) but rather applies to all wrapper functions: i.e., if there were classes named
A,
B and
C in the system
libinst.so, all their member functions would get a corresponding wrapper function in
libinstw.so and thus all of them would call into
wrapper().
2.4. Hooks table
----------------
At the core of the hook system is the hooks table. It stores the three function pointers (pre, system libinst.so and post functions) along with other information. You may consult the source code for those details.
2.5. Hooking up (also: registration)
------------------------------------
For a wrapper function to call any of the three function pointers, it must be hooked up first into the hooks table.
The function that accomplishes this is invoked in
reg.c and is called
register_wrapper_function():
Code:
if (register_wrapper_function(0,
(void *) iResource_destroy_void_4739_pre,
(void *) LIBINSTW_DEFAULT_INTERNAL,
(void *) iResource_destroy_void_4739_post) < 1)
{
return 0; // ERROR: Couldn't register hook functions for iResource::destroy(void)
}
The first parameter is an index into the hooks table. Every function that can be hooked up already has an entry in this table because the
hooks_table.cxx and
reg.c files are generated together in
add_new_wrappers.pl.
This is discussed later.
2.6. Function arguments
-----------------------
There is one last hurdle to overcome: function arguments.
First, why is this a problem? Because...
* There are many functions (in total
4975, including
free functions)
* They have potentially different number of arguments
Ideally, a single centralized function would dispatch arguments to the corresponding pre, system
libinst.so or post functions. Let's see if this is somehow possible.
There are two relevant layers where argument passing takes place:
1.
C++ or language layer
2.
ABI or binary layer
On the
C++ or language layer, each function has to be handled separately or, at least, all those functions with the same arguments. This was exactly one of the reasons listed above of why this should be solved differently: it is time consuming and bloats the source code with function argument handling code, so it isn't efficient either.
On the
ABI level, however, there is an interesting convention that is part of the
ABI:
Calling convention. More specifically: function calling convention, which includes the required mechanisms to pass arguments from the caller to the callee and back again.
On
SGI IRIX/MIPS, function arguments (the first 8 arguments) are passed using 8 registers, numbered
r4-r11 (symbolically named
a0-a7).
Return values are passed back using two registers,
r2-r3 (symbolically named
v0-v1).
And what happens with functions that need more than 8 parameters I hear you thinking?
They are passed on the stack.
And if a function returns data that's larger than two 64 bit registers (
v0,
v1) ?
Then only one pointer (passed in
a0, and returned in
v0) is passed to the function and back again. This pointer contains the address of the destination memory block.
Since there are no functions with more than 8 parameters in
libinst.so, that's one down. Passing data back is, again, handled by one single register,
v0. Another one that bites the dust.
This leaves an elegant solution very close at hand, if it wasn't for a little detail.
To touch code at assembly level, it must be coded at assembly level. This holds true for
SGI MIPSpro C/C++ where no
asm() construct is available to my knowledge.
So, let's dig a bit deeper.
The following illustration shows the normal
SGI MIPSpro C/C++ (version 7.4.4) compiler driver phases (left):
It is beyond the scope of this write-up to explain each phase in detail, but the avid reader may consult the splendid documentation on irix7:
https://www.irix7.com/techpubs.html
The right side shows where exactly the driver must be intercepted to modify assembly code, before it is processed any further.
What must be done is to change the file with all wrapper functions (
wrappers.cxx) in its assembly phase,
wrappers.s, and then just continue the compile process with the modified file.
This modification on assembly level and subsequent linking is automated with two scripts called
asm.pl and
LCC.ksh, respectively. The relevant part in the
Makefile is:
Code:
wrappers.s: wrappers.cxx
# wrappers.s must be run through asm.pl to generate
# modified wrappers.s usable for linking into libinstw.so
CC -S -v -g3 -O0 -shared wrappers.cxx
./asm.pl wrappers.s
...
libinstw.so: libinstw.cxx wrappers.s asm.s
mv wrappers.s wrappers.s.bak
gmake wrappers.s
CC -g3 -O0 -shared -c libinstw.cxx -o libinstw.o # libinstw.cxx -> libinstw.s
./LCC.ksh # libinstw.s -> libinstw.o -> ... -> libinstw.so
So, to get
libinstw.so,
wrappers.cxx is first compiled up to the asm phase, then modified using
asm.pl, and then linked into
libinstw.so in
LCC.ksh. That's it!
3. Putting it all together
==========================
The logical components of the source code for
inst v1.0.0 are presented next:
The distinction between
C and
C++ comes from one of the requirements of the bounty. The code should compile and be compliant with
C89, so the code base has been split into the part handling the
C++ side of libinst.so, and the
C part, which is what meets the requirement.
All pre and post functions are on the
C side of life, this is where you would put your own code into.
The arrows indicate three files that need to be mirrored from
C++ to
C and slightly modified. Their modifications are trivial but saved some time.
3.1 add_new_wrappers.pl
-----------------------
Most of the time for this project was used to actually parse the function signatures from
libinst.so. This parsing was essential to creating
wrappers.hxx, the most important file of the project. It contains the class type definitions of all class types used in the
4975 functions, plus it places them according to their
interdependencies. Some minor tweaks had to be done manually to round this up.
With the class type definitions in place, any libinst.so function can be intercepted or wrapped in
wrappers.cxx just by including appropriate definitions in
wrappers.cxx for the wrapper function itself,
hooks_table.cxx for the corresponding hook entry,
prepost.[ch] for the pre and post functions and finally
reg.c where the registration function is called to hook the pre and post functions into
libinstw.so.
The following image shows the big picture:
The two pairs of files to the left and right of the
add_new_wrappers.pl script are temporary files used to generate the final
hooks_table.cxx and
wrappers.cxx files, respectively.
To use this script, it needs the signatures of the functions in
libinst.so that should be wrapped and included in the above files. These signatures are found in the decompiler/ subdirectory in the files named funcs.all (all
C/C++ function signatures),
funcs.final (all
C++ function signatures), and
funcs.final.lite (the reduced version that was actually used).
The last file resulted from the fact that some of the functions were not necessary from a functional point of view but were expensive in terms of performance. Examples include all constructors, destructors, operators plus some functions that made the final
inst v1.0.0 too slow.
Using the above script and modifying the
funcs.* file, any function can be wrapped.
Note that there were some functions that did not follow the expected
ABI calling convention (manual or automated optimizations?) and would therefore require some more work.
Nonetheless, these few functions were pretty useless and interceptable in prior function invocations or they could also be replaced entirely in
reg.c with a user defined one.
4. How to build
===============
If a different number of signatures is required, first invoke
add_new_wrappers.pl:
Code:
bash# ./add_new_wrappers.pl decompiler/funcs.modified > out
The
out file contains the same generated output but is more easily debuggable.
Once the output files are generated, just:
The executable and the library can be generated separately using targets
inst and
libinstw.so.
5. New flags
============
5.1 Tracelogging
----------------
Using wrapper, pre and post functions means that execution can be tightly "controlled" at any point in time. I found it useful to implement a new feature to facilitate debugging extensions or general execution of inst.
The flag is called the tracelog flag, enabled by
-t or
-T. The syntax is:
Code:
bash# inst -t/-T /path/to/tracelog/file
When invoked with tracelogging enabled, inst logs all invocations of wrapper functions just before calling the pre function. It also synchronizes writes with the tracelog file such that it does not return unless the name of the function was written to the file.
This slows down the execution of inst, but helps to pinpoint where exactly a function is not being invoked and therefore quickly find functions that are incompatible with the hooks system. There were very few of them, but they would not have been found without this tracelogging feature.
With
-t, only function names are logged. With
-T, function names and their argument registers are logged.
5.2 bsdtar support
------------------
To support
bsdtar, a new flag has been added,
-b:
Code:
bash# inst -b -f /path/to/source_0 [-f /path/to/source_N ...]
When used, any subsequent
-f flag to specify a source distribution that contains the pathname of a file, is passed first through
bsdtar with the
-x flag. This allows to use any format supported by
bsdtar with
inst v1.0.0 from the command line.
The executable is found using the name
bsdtar as typed in a shell, thus using
PATH. If any other
bsdtar executable should be used, the
INST_BSDTAR environment variable can be set to point to this executable before invoking inst.
Both flags are documented in the manpage for
inst v1.0.0.
6. Extensions
=============
There are three approaches to extension writing for
inst v1.0.0:
1. Stage 1
2. Stage 2
3. Hybrid
As depicted in the very first image,
inst is invoked from an
inst.c file that calls into the
libinstw.cxx file to finally invoke the system
libinst.so function.
Stage 1 would be
inst.c. It allows to add any code that wishes to manipulate command line
arguments before passing them over to
libinstw.cxx/libinst.so.
Stage 2 refers to the pre, libinst.so, and post functions. Any combination of them can be used to implement the extension. Arguments to the system
libinst.so function are passed to the pre and post functions too, so this gives you full access to everything a function has to offer.
The third one is a combination of both stages.
For an example of Stage 1 extension, look at the
bsdtar extension in
inst.c.
These approaches allow you to, basically, implement anything.
EDIT_0: Corrected indentation.
EDIT_1: Added missing image in
2.3 Improving wrapper functions (thanks to Robespierre!). For links to git repositories, see posts below (thanks to Raion!).