GP-5263: Update Debugger GhidraClass
|
@ -34,6 +34,7 @@ import docking.action.DockingActionIf;
|
|||
import docking.widgets.fieldpanel.FieldPanel;
|
||||
import generic.Unique;
|
||||
import generic.jar.ResourceFile;
|
||||
import ghidra.GhidraTestApplicationLayout;
|
||||
import ghidra.app.cmd.disassemble.DisassembleCommand;
|
||||
import ghidra.app.context.ProgramLocationActionContext;
|
||||
import ghidra.app.decompiler.component.DecompilerPanel;
|
||||
|
@ -109,6 +110,7 @@ import ghidra.util.Msg;
|
|||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.ConsoleTaskMonitor;
|
||||
import help.screenshot.GhidraScreenShotGenerator;
|
||||
import utility.application.ApplicationLayout;
|
||||
|
||||
public class TutorialDebuggerScreenShots extends GhidraScreenShotGenerator
|
||||
implements AsyncTestUtils {
|
||||
|
@ -145,6 +147,18 @@ public class TutorialDebuggerScreenShots extends GhidraScreenShotGenerator
|
|||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected ApplicationLayout createApplicationLayout() throws IOException {
|
||||
return new GhidraTestApplicationLayout(new File(getTestDirectoryPath())) {
|
||||
@Override
|
||||
protected Set<String> getDependentModulePatterns() {
|
||||
Set<String> patterns = super.getDependentModulePatterns();
|
||||
patterns.add("Debugger-agent");
|
||||
return patterns;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TestEnv newTestEnv() throws Exception {
|
||||
return env = new MyTestEnv("DebuggerCourse");
|
||||
|
@ -734,7 +748,7 @@ public class TutorialDebuggerScreenShots extends GhidraScreenShotGenerator
|
|||
mappings.addModuleMappings(proposal.computeMap().values(), monitor, true);
|
||||
}
|
||||
|
||||
waitForCondition(() -> flatDbg.translateDynamicToStatic(dynAddr) != null);
|
||||
//waitForCondition(() -> flatDbg.translateDynamicToStatic(dynAddr) != null);
|
||||
|
||||
runSwing(() -> tool.setSize(1920, 1080));
|
||||
captureProvider(DebuggerStaticMappingProvider.class);
|
||||
|
|
|
@ -152,12 +152,16 @@ id="toc-customized-launching">Customized Launching</a></li>
|
|||
<li><a href="#exercise-launch-with-command-line-help"
|
||||
id="toc-exercise-launch-with-command-line-help">Exercise: Launch with
|
||||
Command-line Help</a></li>
|
||||
<li><a href="#attaching" id="toc-attaching">Attaching</a></li>
|
||||
<li><a href="#exercise-attach" id="toc-exercise-attach">Exercise:
|
||||
Attach</a></li>
|
||||
<li><a href="#attaching" id="toc-attaching">Attaching</a>
|
||||
<ul>
|
||||
<li><a href="#troubleshooting-1"
|
||||
id="toc-troubleshooting-1">Troubleshooting</a></li>
|
||||
</ul></li>
|
||||
<li><a href="#exercise-attach" id="toc-exercise-attach">Exercise:
|
||||
Attach</a></li>
|
||||
<li><a href="#troubleshooting-2"
|
||||
id="toc-troubleshooting-2">Troubleshooting</a></li>
|
||||
</ul></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<section id="getting-started" class="level1">
|
||||
|
@ -453,13 +457,14 @@ target waits for input.</p>
|
|||
<li>Run <code>termmines</code> in a terminal outside of Ghidra with the
|
||||
desired command-line parameters.</li>
|
||||
<li>In the Ghidra Debugger, use the <strong>Launch</strong> button
|
||||
drop-down and select <strong>Configured and Launch termmines using… →
|
||||
raw gdb</strong>. The “raw” connector will give us a GDB session without
|
||||
a target.</li>
|
||||
drop-down and select <strong>Configure and Launch termmines using… →
|
||||
gdb</strong>.</li>
|
||||
<li>Clear the <strong>Image</strong> field to configure a GDB session
|
||||
without a target.</li>
|
||||
<li>Ghidra needs to know the location of gdb and the architecture of the
|
||||
intended target. The defaults are correct for 64-bit x86 targets using
|
||||
the system’s copy of GDB. Probably, you can just click
|
||||
<strong>Launch</strong>.</li>
|
||||
the system’s copy of GDB.</li>
|
||||
<li>Click <strong>Launch</strong>.</li>
|
||||
<li>In the <strong>Model</strong> window (to the left), expand the
|
||||
<em>Available</em> node.</li>
|
||||
<li>In the filter box, type <code>termmines</code>.</li>
|
||||
|
@ -467,6 +472,17 @@ the system’s copy of GDB. Probably, you can just click
|
|||
you prefer, note the PID, e.g. 1234, then in the
|
||||
<strong>Terminal</strong> type, e.g., <code>attach 1234</code>.</li>
|
||||
</ol>
|
||||
<p><strong>TIP</strong>: In later exercises, you may use the
|
||||
<strong>Reset</strong> button to re-populate the default value for the
|
||||
<strong>Image</strong> field. Be sure to change <strong>Run
|
||||
Command</strong> back to “start”, though.</p>
|
||||
<section id="troubleshooting-1" class="level3">
|
||||
<h3>Troubleshooting</h3>
|
||||
<p>If the <strong>Model</strong> window is blank, check for a “noname”
|
||||
tab in the Dynamic Listing, and click it.</p>
|
||||
<p>If the <strong>Model</strong> window seems incomplete after
|
||||
attaching, check that its Filter box is cleared.</p>
|
||||
</section>
|
||||
</section>
|
||||
<section id="exercise-attach" class="level2">
|
||||
<h2>Exercise: Attach</h2>
|
||||
|
@ -476,7 +492,7 @@ in <code>read</code> you have completed this exercise. Quit GDB from the
|
|||
<strong>Terminal</strong> before proceeding to the next module: <a
|
||||
href="A2-UITour.html">A Tour of the UI</a></p>
|
||||
</section>
|
||||
<section id="troubleshooting-1" class="level2">
|
||||
<section id="troubleshooting-2" class="level2">
|
||||
<h2>Troubleshooting</h2>
|
||||
<p>If you get <code>Operation not permitted</code> or similar when
|
||||
trying to attach, it is likely your Linux system is configured with
|
||||
|
|
|
@ -219,15 +219,24 @@ when using Trace RMI.
|
|||
Note this technique is only possible because the target waits for input.
|
||||
|
||||
1. Run `termmines` in a terminal outside of Ghidra with the desired command-line parameters.
|
||||
1. In the Ghidra Debugger, use the **Launch** button drop-down and select **Configured and Launch termmines using... → raw gdb**.
|
||||
The "raw" connector will give us a GDB session without a target.
|
||||
1. In the Ghidra Debugger, use the **Launch** button drop-down and select **Configure and Launch termmines using... → gdb**.
|
||||
1. Clear the **Image** field to configure a GDB session without a target.
|
||||
1. Ghidra needs to know the location of gdb and the architecture of the intended target.
|
||||
The defaults are correct for 64-bit x86 targets using the system's copy of GDB.
|
||||
Probably, you can just click **Launch**.
|
||||
1. Click **Launch**.
|
||||
1. In the **Model** window (to the left), expand the *Available* node.
|
||||
1. In the filter box, type `termmines`.
|
||||
1. Right click on the node and select **Attach**, or, if you prefer, note the PID, e.g. 1234, then in the **Terminal** type, e.g., `attach 1234`.
|
||||
|
||||
**TIP**: In later exercises, you may use the **Reset** button to re-populate the default value for the **Image** field.
|
||||
Be sure to change **Run Command** back to "start", though.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
If the **Model** window is blank, check for a "noname" tab in the Dynamic Listing, and click it.
|
||||
|
||||
If the **Model** window seems incomplete after attaching, check that its Filter box is cleared.
|
||||
|
||||
## Exercise: Attach
|
||||
|
||||
Try attaching on your own, if you have not already.
|
||||
|
|
|
@ -312,7 +312,10 @@ forward a single instruction each time you press it. Also notice that
|
|||
the Static Listing moves with the Dynamic Listing. You may navigate in
|
||||
either listing, and so long as there is a corresponding location in the
|
||||
other, the two will stay synchronized. You may also open the Decompiler
|
||||
just as you would in the CodeBrowser, and it will stay in sync too.</p>
|
||||
just as you would in the CodeBrowser, and it will stay in sync too.
|
||||
<strong>TIP</strong>: If you get lost in memory, you can seek back to
|
||||
the program counter by double-clicking “pc = …” in the top right of the
|
||||
listing.</p>
|
||||
<p>When you have clicked <img src="images/stepinto.png"
|
||||
alt="step into" /> <strong>Step Into</strong> a sufficient number of
|
||||
times, you should end up in a subroutine. You can click <img
|
||||
|
|
|
@ -142,6 +142,7 @@ Notice that the Dynamic Listing moves forward a single instruction each time you
|
|||
Also notice that the Static Listing moves with the Dynamic Listing.
|
||||
You may navigate in either listing, and so long as there is a corresponding location in the other, the two will stay synchronized.
|
||||
You may also open the Decompiler just as you would in the CodeBrowser, and it will stay in sync too.
|
||||
**TIP**: If you get lost in memory, you can seek back to the program counter by double-clicking "pc = ..." in the top right of the listing.
|
||||
|
||||
When you have clicked  **Step Into** a sufficient number of times, you should end up in a subroutine.
|
||||
You can click  **Step Out** to leave the subroutine.
|
||||
|
|
|
@ -273,7 +273,9 @@ toggle action, press <strong><code>K</code></strong> on the keyboard, or
|
|||
double-click its icon in the margin.</li>
|
||||
<li>From the Model window, expand the <em>Breakpoints</em> node and
|
||||
double-click a breakpoint, or select one with the keyboard and press
|
||||
<strong><code>ENTER</code></strong>.</li>
|
||||
<strong><code>ENTER</code></strong>. For GDB, this must be done from the
|
||||
top-level <em>Breakpoints</em> node, not the one subordinate to the
|
||||
<em>inferior</em>.</li>
|
||||
<li>From the Breakpoints window, single-click the breakpoint’s status
|
||||
icon, right-click an entry and select a toggle action, or create a
|
||||
selection and use a toggling action from the local toolbar. Either panel
|
||||
|
@ -340,7 +342,8 @@ synchronized after importing libc</figcaption>
|
|||
<section id="troubleshooting" class="level4">
|
||||
<h4>Troubleshooting</h4>
|
||||
<p>If it seems nothing has changed, except now you have a second program
|
||||
database open, then the new module may not be successfully mapped.</p>
|
||||
database open, then the new module may not be successfully mapped. Try
|
||||
one or more of the following:</p>
|
||||
<ol type="1">
|
||||
<li>Re-check the Debug Console window and verify the note has been
|
||||
removed.</li>
|
||||
|
@ -349,6 +352,9 @@ system, so the name of the module and the name of the program database
|
|||
do not match.</li>
|
||||
<li>Ensure that <code>libc</code> is the current program (tab) in the
|
||||
Static Listing.</li>
|
||||
<li>Wait for auto-analysis of <code>libc</code> to complete. Yeah, it
|
||||
may take a moment, but auto-mapping is queued as a background task, and
|
||||
so it cannot map things until auto-analysis is done.</li>
|
||||
<li>In the Modules window, right-click on <code>libc</code>, and select
|
||||
<strong>Map Module to libc</strong>. (Names and titles will likely
|
||||
differ.)</li>
|
||||
|
@ -446,7 +452,7 @@ boards for any <code>termmines</code> session.</p>
|
|||
<p>Write a program that takes a seed from the user and prints a diagram
|
||||
of the first game board with the mines indicated. Optionally, have it
|
||||
print each subsequent game board when the user presses
|
||||
<strong>ENTER</strong>. Check your work by re-launching
|
||||
<strong><code>ENTER</code></strong>. Check your work by re-launching
|
||||
<code>termmines</code>, capturing its seed, inputting it into your
|
||||
program, and then winning the game. Optionally, win 2 more games in the
|
||||
same session.</p>
|
||||
|
|
|
@ -96,6 +96,7 @@ There are several ways to toggle a breakpoint:
|
|||
|
||||
1. In any listing, as in setting a breakpoint, right-click and select a toggle action, press **`K`** on the keyboard, or double-click its icon in the margin.
|
||||
1. From the Model window, expand the *Breakpoints* node and double-click a breakpoint, or select one with the keyboard and press **`ENTER`**.
|
||||
For GDB, this must be done from the top-level *Breakpoints* node, not the one subordinate to the *inferior*.
|
||||
1. From the Breakpoints window, single-click the breakpoint's status icon, right-click an entry and select a toggle action, or create a selection and use a toggling action from the local toolbar.
|
||||
Either panel works, but the top panel is preferred to keep the breakpoints consistent.
|
||||
The local toolbar also has actions for toggling all breakpoints in the session.
|
||||
|
@ -138,10 +139,13 @@ Once imported, the Breakpoints window should update to reflect the static addres
|
|||
#### Troubleshooting
|
||||
|
||||
If it seems nothing has changed, except now you have a second program database open, then the new module may not be successfully mapped.
|
||||
Try one or more of the following:
|
||||
|
||||
1. Re-check the Debug Console window and verify the note has been removed.
|
||||
1. If not, it might be because the module is symlinked in the file system, so the name of the module and the name of the program database do not match.
|
||||
1. Ensure that `libc` is the current program (tab) in the Static Listing.
|
||||
1. Wait for auto-analysis of `libc` to complete.
|
||||
Yeah, it may take a moment, but auto-mapping is queued as a background task, and so it cannot map things until auto-analysis is done.
|
||||
1. In the Modules window, right-click on `libc`, and select **Map Module to libc**. (Names and titles will likely differ.)
|
||||
|
||||
### Capturing the Random Seed
|
||||
|
@ -214,6 +218,6 @@ Because, as we have now confirmed, `termmines` is importing its random number ge
|
|||
Further, because we can capture the seed, and we know the placement algorithm, we can perfectly replicate the sequence of game boards for any `termmines` session.
|
||||
|
||||
Write a program that takes a seed from the user and prints a diagram of the first game board with the mines indicated.
|
||||
Optionally, have it print each subsequent game board when the user presses **ENTER**.
|
||||
Optionally, have it print each subsequent game board when the user presses **`ENTER`**.
|
||||
Check your work by re-launching `termmines`, capturing its seed, inputting it into your program, and then winning the game.
|
||||
Optionally, win 2 more games in the same session.
|
||||
|
|
|
@ -195,8 +195,8 @@ pointer there, just like you would in the Static Listing. You can now
|
|||
navigate to that address by double-clicking it. To return to the stack
|
||||
pointer, you can use the back arrow in the global toolbar, you can click
|
||||
the <img src="images/register-marker.png" alt="track location" /> Track
|
||||
Location button, or you can double-click the <code>sp = [Address]</code>
|
||||
label in the top right of the Dynamic Listing.</p>
|
||||
Location button, or you can double-click the “sp = …” label in the top
|
||||
right of the Dynamic Listing.</p>
|
||||
<p>To examine a more complicated stack segment, we will break at
|
||||
<code>rand</code>. Ensure your breakpoint at <code>rand</code> is
|
||||
enabled and press <img src="images/resume.png" alt="resume" /> Resume.
|
||||
|
@ -295,6 +295,13 @@ section of <code>termmines</code> in the Static Listing, the Dynamic
|
|||
Listing will follow along showing you the live values in memory. You can
|
||||
also experiment by placing code units in the Dynamic Listing before
|
||||
committing to them in the Static Listing.</p>
|
||||
<p><strong>NOTE</strong>: There’s a known issue with auto-seek obtruding
|
||||
user navigation in the listings. In most cases, just navigating again
|
||||
will make it stick. If it becomes a real annoyance, set the
|
||||
<strong>Auto-Track</strong> drop-down in the top right of the Dynamic
|
||||
Listing to <strong>Do Not Track</strong> while you’re doing static RE.
|
||||
Be sure to put it back to <strong>Track Program Counter</strong> when
|
||||
you are done.</p>
|
||||
<section id="questions" class="level4">
|
||||
<h4>Questions:</h4>
|
||||
<ol type="1">
|
||||
|
@ -450,7 +457,7 @@ address is into <code>main</code>, you could use
|
|||
href="../../../Ghidra/Features/Decompiler/src/main/doc/sleigh.xml">Sleigh
|
||||
documentation</a>.</p>
|
||||
<p>Sleigh is a bit unconventional in that its operators are typed rather
|
||||
than its variables. All variables are fix-length bit vectors. Their
|
||||
than its variables. All variables are fixed-length bit vectors. Their
|
||||
sizes are specified in bytes, but they have no other type
|
||||
information.</p>
|
||||
<section id="variables-and-constants" class="level3">
|
||||
|
|
|
@ -68,7 +68,7 @@ Since the target has just entered `main`, we should expect a return address at t
|
|||
With your cursor at the stack pointer, press **`P`** to place a pointer there, just like
|
||||
you would in the Static Listing.
|
||||
You can now navigate to that address by double-clicking it.
|
||||
To return to the stack pointer, you can use the back arrow in the global toolbar, you can click the  Track Location button, or you can double-click the `sp = [Address]` label in the top right of the Dynamic Listing.
|
||||
To return to the stack pointer, you can use the back arrow in the global toolbar, you can click the  Track Location button, or you can double-click the "sp = ..." label in the top right of the Dynamic Listing.
|
||||
|
||||
To examine a more complicated stack segment, we will break at `rand`.
|
||||
Ensure your breakpoint at `rand` is enabled and press  Resume.
|
||||
|
@ -137,6 +137,11 @@ Because you are in a dynamic session, you have an example board to work with.
|
|||
As you navigate the `.data` section of `termmines` in the Static Listing, the Dynamic Listing will follow along showing you the live values in memory.
|
||||
You can also experiment by placing code units in the Dynamic Listing before committing to them in the Static Listing.
|
||||
|
||||
**NOTE**: There's a known issue with auto-seek obtruding user navigation in the listings.
|
||||
In most cases, just navigating again will make it stick.
|
||||
If it becomes a real annoyance, set the **Auto-Track** drop-down in the top right of the Dynamic Listing to **Do Not Track** while you're doing static RE.
|
||||
Be sure to put it back to **Track Program Counter** when you are done.
|
||||
|
||||
#### Questions:
|
||||
|
||||
1. How are the cells allocated?
|
||||
|
@ -244,7 +249,7 @@ For example, to see how far a return address is into `main`, you could use `*:8
|
|||
For the complete specification, see the Semantic Section in the [Sleigh documentation](../../../Ghidra/Features/Decompiler/src/main/doc/sleigh.xml).
|
||||
|
||||
Sleigh is a bit unconventional in that its operators are typed rather than its variables.
|
||||
All variables are fix-length bit vectors.
|
||||
All variables are fixed-length bit vectors.
|
||||
Their sizes are specified in bytes, but they have no other type information.
|
||||
|
||||
### Variables and Constants
|
||||
|
|
|
@ -676,8 +676,7 @@ Invalidate Emulator Cache</strong>.</li>
|
|||
<strong>Resume</strong>.</li>
|
||||
</ol>
|
||||
<p>Stubbing any remaining external calls is left as an exercise. You are
|
||||
successful when the emulator crashes with
|
||||
<code>pc = 00000000</code>.</p>
|
||||
successful when the emulator crashes with “pc = 00000000”.</p>
|
||||
<p>Clear or disable your breakpoint and invalidate the emulator cache
|
||||
again before proceeding to the next technique.</p>
|
||||
</section>
|
||||
|
@ -718,7 +717,7 @@ write the Sleigh code to mimic a <code>RET</code>. As with the
|
|||
<code>CALL</code> override technique, you must now invalidate the
|
||||
emulator cache and resume. Stubbing any remaining external functions is
|
||||
left as an exercise. You are successful when the emulator crashes with
|
||||
<code>pc = 00000000</code>.</p>
|
||||
“pc = 00000000”.</p>
|
||||
</section>
|
||||
</section>
|
||||
<section id="wrapping-up" class="level3">
|
||||
|
|
|
@ -377,7 +377,7 @@ After you have written your Sleigh code:
|
|||
1. Click  **Resume**.
|
||||
|
||||
Stubbing any remaining external calls is left as an exercise.
|
||||
You are successful when the emulator crashes with `pc = 00000000`.
|
||||
You are successful when the emulator crashes with "pc = 00000000".
|
||||
|
||||
Clear or disable your breakpoint and invalidate the emulator cache again before proceeding to the next technique.
|
||||
|
||||
|
@ -412,7 +412,7 @@ return [RIP];
|
|||
Notice that we cannot just write `RET`, but instead must write the Sleigh code to mimic a `RET`.
|
||||
As with the `CALL` override technique, you must now invalidate the emulator cache and resume.
|
||||
Stubbing any remaining external functions is left as an exercise.
|
||||
You are successful when the emulator crashes with `pc = 00000000`.
|
||||
You are successful when the emulator crashes with "pc = 00000000".
|
||||
|
||||
### Wrapping Up
|
||||
|
||||
|
|
|
@ -168,17 +168,14 @@ class="sourceCode numberSource java numberLines"><code class="sourceCode java"><
|
|||
<span id="cb1-6"><a href="#cb1-6"></a> <span class="kw">protected</span> <span class="dt">void</span> <span class="fu">run</span><span class="op">()</span> <span class="kw">throws</span> <span class="bu">Exception</span> <span class="op">{</span></span>
|
||||
<span id="cb1-7"><a href="#cb1-7"></a> <span class="op">}</span></span>
|
||||
<span id="cb1-8"><a href="#cb1-8"></a><span class="op">}</span></span></code></pre></div>
|
||||
<p><strong>NOTE</strong>: The scripting API has been refactored a little
|
||||
since the transition from Recorder-based to TraceRmi-based targets.
|
||||
Parts of the API that are back-end agnostic are accessible from the
|
||||
<p><strong>NOTE</strong>: The scripting API has been refactored since
|
||||
the transition from Recorder-based to TraceRmi-based targets. Parts of
|
||||
the API that are back-end agnostic are accessible from the
|
||||
<code>FlatDebuggerAPI</code> interface. Parts of the API that require a
|
||||
specific back end are in <code>FlatDebuggerRmiAPI</code> and
|
||||
<code>FlatDebuggerRecorderAPI</code>, the latter of which is deprecated.
|
||||
If a script written for version 11.0.2 or prior is not compiling, it can
|
||||
most likely be patched up by changing
|
||||
<code>implements FlatDebuggerAPI</code> to
|
||||
<code>implements FlatDebuggerRecorderAPI</code>, but we recommend
|
||||
porting it to use <code>implements FlatDebuggerRmiAPI</code>.</p>
|
||||
specific back end are in <code>FlatDebuggerRmiAPI</code>. The old
|
||||
<code>FlatDebuggerRecorderAPI</code> was removed in Ghidra 11.3, and
|
||||
scripts needing it should be ported to
|
||||
<code>FlatDebuggerRmiAPI</code>.</p>
|
||||
<p>Technically, the Debugger’s “deep” API is accessible to scripts;
|
||||
however, the flat API is preferred for scripting. Also, the flat API is
|
||||
usually more stable than the deep API. However, because the dynamic
|
||||
|
|
|
@ -24,10 +24,10 @@ public class DemoDebuggerScript extends GhidraScript implements FlatDebuggerAPI
|
|||
}
|
||||
```
|
||||
|
||||
**NOTE**: The scripting API has been refactored a little since the transition from Recorder-based to TraceRmi-based targets.
|
||||
**NOTE**: The scripting API has been refactored since the transition from Recorder-based to TraceRmi-based targets.
|
||||
Parts of the API that are back-end agnostic are accessible from the `FlatDebuggerAPI` interface.
|
||||
Parts of the API that require a specific back end are in `FlatDebuggerRmiAPI` and `FlatDebuggerRecorderAPI`, the latter of which is deprecated.
|
||||
If a script written for version 11.0.2 or prior is not compiling, it can most likely be patched up by changing `implements FlatDebuggerAPI` to `implements FlatDebuggerRecorderAPI`, but we recommend porting it to use `implements FlatDebuggerRmiAPI`.
|
||||
Parts of the API that require a specific back end are in `FlatDebuggerRmiAPI`.
|
||||
The old `FlatDebuggerRecorderAPI` was removed in Ghidra 11.3, and scripts needing it should be ported to `FlatDebuggerRmiAPI`.
|
||||
|
||||
Technically, the Debugger's "deep" API is accessible to scripts; however, the flat API is preferred for scripting.
|
||||
Also, the flat API is usually more stable than the deep API.
|
||||
|
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 23 KiB |