mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-18 00:30:56 +00:00
initial commit
This commit is contained in:
20
.editorconfig
Normal file
20
.editorconfig
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
|
||||||
|
charset = utf-8
|
||||||
|
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
tab_width = 4
|
||||||
|
|
||||||
|
[*.{md,json,yml,props,csproj}]
|
||||||
|
|
||||||
|
indent_size = 2
|
||||||
|
tab_width = 2
|
||||||
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
[Dd]ebug/
|
||||||
|
[Rr]elease/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Pp]ublish/
|
||||||
|
[Aa]rtifacts/
|
||||||
|
logs/
|
||||||
|
.tmp/
|
||||||
|
|
||||||
|
x86/
|
||||||
|
x64/
|
||||||
|
arm64/
|
||||||
|
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*.log
|
||||||
|
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
packages/
|
||||||
|
*.nupkg
|
||||||
|
.nuget/
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
ehthumbs.db
|
||||||
35
Directory.Build.props
Normal file
35
Directory.Build.props
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||||
|
|
||||||
|
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
||||||
|
|
||||||
|
<BaseIntermediateOutputPath>$(RepoRoot)artifacts/obj/$(MSBuildProjectName)/</BaseIntermediateOutputPath>
|
||||||
|
<BaseOutputPath>$(RepoRoot)artifacts/bin/</BaseOutputPath>
|
||||||
|
|
||||||
|
<AppendConfigurationToOutputPath>true</AppendConfigurationToOutputPath>
|
||||||
|
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
||||||
|
<AppendRuntimeIdentifierToOutputPath>true</AppendRuntimeIdentifierToOutputPath>
|
||||||
|
|
||||||
|
<PublishDir>$(RepoRoot)artifacts/publish/$(MSBuildProjectName)/$(Configuration)/$(TargetFramework)/</PublishDir>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(RuntimeIdentifier)' != ''">
|
||||||
|
<PublishDir>$(RepoRoot)artifacts/publish/$(MSBuildProjectName)/$(Configuration)/$(TargetFramework)/$(RuntimeIdentifier)/</PublishDir>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||||
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
|
<DebugType>none</DebugType>
|
||||||
|
<DebugSymbols>false</DebugSymbols>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
13
Directory.Packages.props
Normal file
13
Directory.Packages.props
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageVersion Include="Iced" Version="1.21.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
339
LICENSE.txt
Normal file
339
LICENSE.txt
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your
|
||||||
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
License is intended to guarantee your freedom to share and change free
|
||||||
|
software--to make sure the software is free for all its users. This
|
||||||
|
General Public License applies to most of the Free Software
|
||||||
|
Foundation's software and to any other program whose authors commit to
|
||||||
|
using it. (Some other Free Software Foundation software is covered by
|
||||||
|
the GNU Lesser General Public License instead.) You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
this service if you wish), that you receive source code or can get it
|
||||||
|
if you want it, that you can change the software or use pieces of it
|
||||||
|
in new free programs; and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid
|
||||||
|
anyone to deny you these rights or to ask you to surrender the rights.
|
||||||
|
These restrictions translate to certain responsibilities for you if you
|
||||||
|
distribute copies of the software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must give the recipients all the rights that
|
||||||
|
you have. You must make sure that they, too, receive or can get the
|
||||||
|
source code. And you must show them these terms so they know their
|
||||||
|
rights.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and
|
||||||
|
(2) offer you this license which gives you legal permission to copy,
|
||||||
|
distribute and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain
|
||||||
|
that everyone understands that there is no warranty for this free
|
||||||
|
software. If the software is modified by someone else and passed on, we
|
||||||
|
want its recipients to know that what they have is not the original, so
|
||||||
|
that any problems introduced by others will not reflect on the original
|
||||||
|
authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software
|
||||||
|
patents. We wish to avoid the danger that redistributors of a free
|
||||||
|
program will individually obtain patent licenses, in effect making the
|
||||||
|
program proprietary. To prevent this, we have made it clear that any
|
||||||
|
patent must be licensed for everyone's free use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains
|
||||||
|
a notice placed by the copyright holder saying it may be distributed
|
||||||
|
under the terms of this General Public License. The "Program", below,
|
||||||
|
refers to any such program or work, and a "work based on the Program"
|
||||||
|
means either the Program or any derivative work under copyright law:
|
||||||
|
that is to say, a work containing the Program or a portion of it,
|
||||||
|
either verbatim or with modifications and/or translated into another
|
||||||
|
language. (Hereinafter, translation is included without limitation in
|
||||||
|
the term "modification".) Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of
|
||||||
|
running the Program is not restricted, and the output from the Program
|
||||||
|
is covered only if its contents constitute a work based on the
|
||||||
|
Program (independent of having been made by running the Program).
|
||||||
|
Whether that is true depends on what the Program does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Program's
|
||||||
|
source code as you receive it, in any medium, provided that you
|
||||||
|
conspicuously and appropriately publish on each copy an appropriate
|
||||||
|
copyright notice and disclaimer of warranty; keep intact all the
|
||||||
|
notices that refer to this License and to the absence of any warranty;
|
||||||
|
and give any other recipients of the Program a copy of this License
|
||||||
|
along with the Program.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and
|
||||||
|
you may at your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion
|
||||||
|
of it, thus forming a work based on the Program, and copy and
|
||||||
|
distribute such modifications or work under the terms of Section 1
|
||||||
|
above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices
|
||||||
|
stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in
|
||||||
|
whole or in part contains or is derived from the Program or any
|
||||||
|
part thereof, to be licensed as a whole at no charge to all third
|
||||||
|
parties under the terms of this License.
|
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively
|
||||||
|
when run, you must cause it, when started running for such
|
||||||
|
interactive use in the most ordinary way, to print or display an
|
||||||
|
announcement including an appropriate copyright notice and a
|
||||||
|
notice that there is no warranty (or else, saying that you provide
|
||||||
|
a warranty) and that users may redistribute the program under
|
||||||
|
these conditions, and telling the user how to view a copy of this
|
||||||
|
License. (Exception: if the Program itself is interactive but
|
||||||
|
does not normally print such an announcement, your work based on
|
||||||
|
the Program is not required to print an announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Program,
|
||||||
|
and can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based
|
||||||
|
on the Program, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the
|
||||||
|
entire whole, and thus to each and every part regardless of who wrote it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or
|
||||||
|
collective works based on the Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program
|
||||||
|
with the Program (or with a work based on the Program) on a volume of
|
||||||
|
a storage or distribution medium does not bring the other work under
|
||||||
|
the scope of this License.
|
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it,
|
||||||
|
under Section 2) in object code or executable form under the terms of
|
||||||
|
Sections 1 and 2 above provided that you also do one of the following:
|
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable
|
||||||
|
source code, which must be distributed under the terms of Sections
|
||||||
|
1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three
|
||||||
|
years, to give any third party, for a charge no more than your
|
||||||
|
cost of physically performing source distribution, a complete
|
||||||
|
machine-readable copy of the corresponding source code, to be
|
||||||
|
distributed under the terms of Sections 1 and 2 above on a medium
|
||||||
|
customarily used for software interchange; or,
|
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer
|
||||||
|
to distribute corresponding source code. (This alternative is
|
||||||
|
allowed only for noncommercial distribution and only if you
|
||||||
|
received the program in object code or executable form with such
|
||||||
|
an offer, in accord with Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For an executable work, complete source
|
||||||
|
code means all the source code for all modules it contains, plus any
|
||||||
|
associated interface definition files, plus the scripts used to
|
||||||
|
control compilation and installation of the executable. However, as a
|
||||||
|
special exception, the source code distributed need not include
|
||||||
|
anything that is normally distributed (in either source or binary
|
||||||
|
form) with the major components (compiler, kernel, and so on) of the
|
||||||
|
operating system on which the executable runs, unless that component
|
||||||
|
itself accompanies the executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering
|
||||||
|
access to copy from a designated place, then offering equivalent
|
||||||
|
access to copy the source code from the same place counts as
|
||||||
|
distribution of the source code, even though third parties are not
|
||||||
|
compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program
|
||||||
|
except as expressly provided under this License. Any attempt
|
||||||
|
otherwise to copy, modify, sublicense or distribute the Program is
|
||||||
|
void, and will automatically terminate your rights under this License.
|
||||||
|
However, parties who have received copies, or rights, from you under
|
||||||
|
this License will not have their licenses terminated so long as such
|
||||||
|
parties remain in full compliance.
|
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not
|
||||||
|
signed it. However, nothing else grants you permission to modify or
|
||||||
|
distribute the Program or its derivative works. These actions are
|
||||||
|
prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
modifying or distributing the Program (or any work based on the
|
||||||
|
Program), you indicate your acceptance of this License to do so, and
|
||||||
|
all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Program or works based on it.
|
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the
|
||||||
|
Program), the recipient automatically receives a license from the
|
||||||
|
original licensor to copy, distribute or modify the Program subject to
|
||||||
|
these terms and conditions. You may not impose any further
|
||||||
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
You are not responsible for enforcing compliance by third parties to
|
||||||
|
this License.
|
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot
|
||||||
|
distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you
|
||||||
|
may not distribute the Program at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Program by
|
||||||
|
all those who receive copies directly or indirectly through you, then
|
||||||
|
the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under
|
||||||
|
any particular circumstance, the balance of the section is intended to
|
||||||
|
apply and the section as a whole is intended to apply in other
|
||||||
|
circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any
|
||||||
|
such claims; this section has the sole purpose of protecting the
|
||||||
|
integrity of the free software distribution system, which is
|
||||||
|
implemented by public license practices. Many people have made
|
||||||
|
generous contributions to the wide range of software distributed
|
||||||
|
through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
to distribute software through any other system and a licensee cannot
|
||||||
|
impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to
|
||||||
|
be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Program under this License
|
||||||
|
may add an explicit geographical distribution limitation excluding
|
||||||
|
those countries, so that distribution is permitted only in or among
|
||||||
|
countries not thus excluded. In such case, this License incorporates
|
||||||
|
the limitation as if written in the body of this License.
|
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program
|
||||||
|
specifies a version number of this License which applies to it and "any
|
||||||
|
later version", you have the option of following the terms and conditions
|
||||||
|
either of that version or of any later version published by the Free
|
||||||
|
Software Foundation. If the Program does not specify a version number of
|
||||||
|
this License, you may choose any version ever published by the Free Software
|
||||||
|
Foundation.
|
||||||
|
|
||||||
|
10. If you wish to incorporate parts of the Program into other free
|
||||||
|
programs whose distribution conditions are different, write to the author
|
||||||
|
to ask for permission. For software which is copyrighted by the Free
|
||||||
|
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||||
|
make exceptions for this. Our decision will be guided by the two goals
|
||||||
|
of preserving the free status of all derivatives of our free software and
|
||||||
|
of promoting the sharing and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||||
|
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||||
|
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||||
|
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||||
|
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||||
|
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||||
|
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||||
|
REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||||
|
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||||
|
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||||
|
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||||
|
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||||
|
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
convey the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along
|
||||||
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program is interactive, make it output a short notice like this
|
||||||
|
when it starts in an interactive mode:
|
||||||
|
|
||||||
|
Gnomovision version 69, Copyright (C) year name of author
|
||||||
|
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, the commands you use may
|
||||||
|
be called something other than `show w' and `show c'; they could even be
|
||||||
|
mouse-clicks or menu items--whatever suits your program.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||||
|
necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||||
|
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||||
|
|
||||||
|
<signature of Ty Coon>, 1 April 1989
|
||||||
|
Ty Coon, President of Vice
|
||||||
|
|
||||||
|
This General Public License does not permit incorporating your program into
|
||||||
|
proprietary programs. If your program is a subroutine library, you may
|
||||||
|
consider it more useful to permit linking proprietary applications with the
|
||||||
|
library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License.
|
||||||
117
LICENSES/GPL-2.0-or-later.txt
Normal file
117
LICENSES/GPL-2.0-or-later.txt
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.
|
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
|
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:
|
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
|
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.
|
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.
|
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
|
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.
|
||||||
|
|
||||||
|
10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program is interactive, make it output a short notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||||
|
|
||||||
|
signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice
|
||||||
12
REUSE.toml
Normal file
12
REUSE.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
version = 1
|
||||||
|
|
||||||
|
[[annotations]]
|
||||||
|
path = [
|
||||||
|
"REUSE.toml",
|
||||||
|
"**/packages.lock.json",
|
||||||
|
"scripts/ps5_names.txt",
|
||||||
|
"src/SharpEmu.HLE/Aerolib/aerolib.bin"
|
||||||
|
]
|
||||||
|
precedence = "aggregate"
|
||||||
|
SPDX-FileCopyrightText = "SharpEmu Emulator Project"
|
||||||
|
SPDX-License-Identifier = "GPL-2.0-or-later"
|
||||||
14
SharpEmu.slnx
Normal file
14
SharpEmu.slnx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Solution>
|
||||||
|
<Folder Name="/src/">
|
||||||
|
<Project Path="src/SharpEmu.CLI/SharpEmu.CLI.csproj" />
|
||||||
|
<Project Path="src/SharpEmu.Core/SharpEmu.Core.csproj" />
|
||||||
|
<Project Path="src/SharpEmu.HLE/SharpEmu.HLE.csproj" />
|
||||||
|
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
|
||||||
|
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
|
||||||
|
</Folder>
|
||||||
|
</Solution>
|
||||||
53
scripts/generate_aerolib_binary.py
Normal file
53
scripts/generate_aerolib_binary.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import struct
|
||||||
|
import hashlib
|
||||||
|
from base64 import b64encode as base64enc
|
||||||
|
from binascii import unhexlify as uhx
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
NAMES = 'scripts/ps5_names.txt'
|
||||||
|
OUTPUT = 'src/SharpEmu.HLE/Aerolib/aerolib.bin'
|
||||||
|
|
||||||
|
def name2nid(name):
|
||||||
|
symbol = hashlib.sha1(name.encode() + uhx('518D64A635DED8C1E6B039B1C3E55230')).digest()
|
||||||
|
id_val = struct.unpack('<Q', symbol[:8])[0]
|
||||||
|
nid = base64enc(uhx('%016x' % id_val), b'+-').rstrip(b'=')
|
||||||
|
return nid.decode('utf-8')
|
||||||
|
|
||||||
|
def generate():
|
||||||
|
names_path = Path(NAMES)
|
||||||
|
output_path = Path(OUTPUT)
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
with open(names_path, 'r', encoding='utf-8') as f:
|
||||||
|
for line in f:
|
||||||
|
name = line.strip()
|
||||||
|
if name:
|
||||||
|
nid = name2nid(name)
|
||||||
|
entries.append((nid, name))
|
||||||
|
|
||||||
|
print(f"Found {len(entries)} entries")
|
||||||
|
|
||||||
|
data = bytearray()
|
||||||
|
data.extend(struct.pack('<I', len(entries)))
|
||||||
|
|
||||||
|
for nid, name in entries:
|
||||||
|
nid_bytes = nid.encode('utf-8')
|
||||||
|
name_bytes = name.encode('utf-8')
|
||||||
|
data.append(len(nid_bytes))
|
||||||
|
data.extend(nid_bytes)
|
||||||
|
data.extend(struct.pack('<H', len(name_bytes)))
|
||||||
|
data.extend(name_bytes)
|
||||||
|
|
||||||
|
with open(output_path, 'wb') as f:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
print(f"Generated: {output_path} ({len(data):,} bytes)")
|
||||||
|
print(f"Total entries: {len(entries)}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
generate()
|
||||||
148678
scripts/ps5_names.txt
Normal file
148678
scripts/ps5_names.txt
Normal file
File diff suppressed because it is too large
Load Diff
673
src/SharpEmu.CLI/Program.cs
Normal file
673
src/SharpEmu.CLI/Program.cs
Normal file
@@ -0,0 +1,673 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.Core.Runtime;
|
||||||
|
using SharpEmu.Core.Cpu;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Logging;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace SharpEmu.CLI;
|
||||||
|
|
||||||
|
internal static partial class Program
|
||||||
|
{
|
||||||
|
private static readonly SharpEmuLogger Log = SharpEmuLog.For("SharpEmu.CLI");
|
||||||
|
private const int DefaultImportTraceLimit = 32;
|
||||||
|
private const string MitigatedChildFlag = "--sharpemu-mitigated-child";
|
||||||
|
private const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
|
||||||
|
private const uint INFINITE = 0xFFFFFFFF;
|
||||||
|
private const int PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY = 0x00020007;
|
||||||
|
private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
|
||||||
|
private const int JobObjectExtendedLimitInformation = 9;
|
||||||
|
private const ulong PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
|
||||||
|
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF = 0x00000002UL << 28;
|
||||||
|
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32;
|
||||||
|
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
|
||||||
|
|
||||||
|
private static int Main(string[] args)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
|
||||||
|
|
||||||
|
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||||
|
if (!isMitigatedChild && TryRunMitigatedChild(args, out var childExitCode))
|
||||||
|
{
|
||||||
|
return childExitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryParseArguments(args, out var ebootPath, out var runtimeOptions, out var logLevel))
|
||||||
|
{
|
||||||
|
PrintUsage();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
SharpEmuLog.MinimumLevel = logLevel;
|
||||||
|
|
||||||
|
ebootPath = Path.GetFullPath(ebootPath);
|
||||||
|
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
|
||||||
|
|
||||||
|
if (!File.Exists(ebootPath))
|
||||||
|
{
|
||||||
|
Log.Error($"EBOOT file was not found: {ebootPath}");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine("[DEBUG] Creating runtime...");
|
||||||
|
|
||||||
|
using var runtime = SharpEmuRuntime.CreateDefault(runtimeOptions);
|
||||||
|
|
||||||
|
OrbisGen2Result result;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[DEBUG] Running: {ebootPath}");
|
||||||
|
result = runtime.Run(ebootPath);
|
||||||
|
Console.Error.WriteLine($"[DEBUG] Result: {result}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[DEBUG] Exception: {ex}");
|
||||||
|
Log.Error("SharpEmu failed to run.", ex);
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Info($"SharpEmu execution completed. Result={result} (0x{(int)result:X8})");
|
||||||
|
if (!string.IsNullOrWhiteSpace(runtime.LastSessionSummary))
|
||||||
|
{
|
||||||
|
Log.Info(runtime.LastSessionSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(runtime.LastBasicBlockTrace))
|
||||||
|
{
|
||||||
|
Log.Info("BB trace:");
|
||||||
|
Log.Info(runtime.LastBasicBlockTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(runtime.LastMilestoneLog))
|
||||||
|
{
|
||||||
|
Log.Info(runtime.LastMilestoneLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != OrbisGen2Result.ORBIS_GEN2_OK && !string.IsNullOrWhiteSpace(runtime.LastExecutionDiagnostics))
|
||||||
|
{
|
||||||
|
Log.Warn(runtime.LastExecutionDiagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runtimeOptions.ImportTraceLimit > 0 && !string.IsNullOrWhiteSpace(runtime.LastExecutionTrace))
|
||||||
|
{
|
||||||
|
Log.Info("Import trace:");
|
||||||
|
Log.Info(runtime.LastExecutionTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result == OrbisGen2Result.ORBIS_GEN2_OK ? 0 : 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[] NormalizeInternalArguments(string[] args, out bool isMitigatedChild)
|
||||||
|
{
|
||||||
|
isMitigatedChild = false;
|
||||||
|
if (args.Length == 0)
|
||||||
|
{
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
var list = new List<string>(args.Length);
|
||||||
|
foreach (var arg in args)
|
||||||
|
{
|
||||||
|
if (string.Equals(arg, MitigatedChildFlag, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
isMitigatedChild = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Add(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryRunMitigatedChild(string[] args, out int childExitCode)
|
||||||
|
{
|
||||||
|
childExitCode = 0;
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_MITIGATION_RELAUNCH"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var processPath = Environment.ProcessPath;
|
||||||
|
if (string.IsNullOrWhiteSpace(processPath))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var childArgs = new string[args.Length + 1];
|
||||||
|
childArgs[0] = MitigatedChildFlag;
|
||||||
|
for (var i = 0; i < args.Length; i++)
|
||||||
|
{
|
||||||
|
childArgs[i + 1] = args[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandLine = BuildCommandLine(processPath, childArgs);
|
||||||
|
var startupInfoEx = new STARTUPINFOEX();
|
||||||
|
startupInfoEx.StartupInfo.cb = Marshal.SizeOf<STARTUPINFOEX>();
|
||||||
|
|
||||||
|
nint attributeList = 0;
|
||||||
|
nint mitigationPolicies = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
nuint attributeListSize = 0;
|
||||||
|
_ = InitializeProcThreadAttributeList(0, 1, 0, ref attributeListSize);
|
||||||
|
attributeList = Marshal.AllocHGlobal((nint)attributeListSize);
|
||||||
|
if (!InitializeProcThreadAttributeList(attributeList, 1, 0, ref attributeListSize))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
startupInfoEx.lpAttributeList = attributeList;
|
||||||
|
|
||||||
|
var policy1 = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF;
|
||||||
|
var policy2 =
|
||||||
|
PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF |
|
||||||
|
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF |
|
||||||
|
PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF;
|
||||||
|
|
||||||
|
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
|
||||||
|
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
|
||||||
|
Marshal.WriteInt64(nint.Add(mitigationPolicies, sizeof(long)), unchecked((long)policy2));
|
||||||
|
|
||||||
|
if (!UpdateProcThreadAttribute(
|
||||||
|
attributeList,
|
||||||
|
0,
|
||||||
|
(nint)PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY,
|
||||||
|
mitigationPolicies,
|
||||||
|
(nuint)(sizeof(ulong) * 2),
|
||||||
|
0,
|
||||||
|
0))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdLineBuilder = new StringBuilder(commandLine);
|
||||||
|
nint jobHandle = 0;
|
||||||
|
if (!CreateProcessW(
|
||||||
|
processPath,
|
||||||
|
cmdLineBuilder,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
EXTENDED_STARTUPINFO_PRESENT,
|
||||||
|
0,
|
||||||
|
Environment.CurrentDirectory,
|
||||||
|
ref startupInfoEx,
|
||||||
|
out var processInfo))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
jobHandle = CreateJobObjectW(0, null);
|
||||||
|
if (jobHandle != 0 &&
|
||||||
|
TryEnableKillOnJobClose(jobHandle) &&
|
||||||
|
!AssignProcessToJobObject(jobHandle, processInfo.hProcess))
|
||||||
|
{
|
||||||
|
CloseHandle(jobHandle);
|
||||||
|
jobHandle = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConsoleCancelEventHandler? cancelHandler = null;
|
||||||
|
EventHandler? processExitHandler = null;
|
||||||
|
cancelHandler = (_, eventArgs) =>
|
||||||
|
{
|
||||||
|
_ = TerminateProcess(processInfo.hProcess, 1);
|
||||||
|
eventArgs.Cancel = true;
|
||||||
|
};
|
||||||
|
processExitHandler = (_, _) =>
|
||||||
|
{
|
||||||
|
_ = TerminateProcess(processInfo.hProcess, 1);
|
||||||
|
};
|
||||||
|
Console.CancelKeyPress += cancelHandler;
|
||||||
|
AppDomain.CurrentDomain.ProcessExit += processExitHandler;
|
||||||
|
|
||||||
|
_ = WaitForSingleObject(processInfo.hProcess, INFINITE);
|
||||||
|
Console.CancelKeyPress -= cancelHandler;
|
||||||
|
AppDomain.CurrentDomain.ProcessExit -= processExitHandler;
|
||||||
|
|
||||||
|
if (!GetExitCodeProcess(processInfo.hProcess, out var exitCode))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
childExitCode = unchecked((int)exitCode);
|
||||||
|
Console.Error.WriteLine("[DEBUG] Running in mitigated child process (CET/CFG disabled).");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (jobHandle != 0)
|
||||||
|
{
|
||||||
|
CloseHandle(jobHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseHandle(processInfo.hThread);
|
||||||
|
CloseHandle(processInfo.hProcess);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (attributeList != 0)
|
||||||
|
{
|
||||||
|
DeleteProcThreadAttributeList(attributeList);
|
||||||
|
Marshal.FreeHGlobal(attributeList);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mitigationPolicies != 0)
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(mitigationPolicies);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildCommandLine(string processPath, IReadOnlyList<string> args)
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.Append(QuoteArgument(processPath));
|
||||||
|
for (var i = 0; i < args.Count; i++)
|
||||||
|
{
|
||||||
|
builder.Append(' ');
|
||||||
|
builder.Append(QuoteArgument(args[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryEnableKillOnJobClose(nint jobHandle)
|
||||||
|
{
|
||||||
|
var extendedLimitInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
||||||
|
{
|
||||||
|
BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION
|
||||||
|
{
|
||||||
|
LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var size = Marshal.SizeOf<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>();
|
||||||
|
var memory = Marshal.AllocHGlobal(size);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Marshal.StructureToPtr(extendedLimitInfo, memory, false);
|
||||||
|
return SetInformationJobObject(
|
||||||
|
jobHandle,
|
||||||
|
JobObjectExtendedLimitInformation,
|
||||||
|
memory,
|
||||||
|
unchecked((uint)size));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(memory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string QuoteArgument(string argument)
|
||||||
|
{
|
||||||
|
if (argument.Length == 0)
|
||||||
|
{
|
||||||
|
return "\"\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
var needsQuotes = false;
|
||||||
|
foreach (var c in argument)
|
||||||
|
{
|
||||||
|
if (char.IsWhiteSpace(c) || c == '"')
|
||||||
|
{
|
||||||
|
needsQuotes = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!needsQuotes)
|
||||||
|
{
|
||||||
|
return argument;
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder = new StringBuilder(argument.Length + 2);
|
||||||
|
builder.Append('"');
|
||||||
|
|
||||||
|
var backslashCount = 0;
|
||||||
|
foreach (var c in argument)
|
||||||
|
{
|
||||||
|
if (c == '\\')
|
||||||
|
{
|
||||||
|
backslashCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == '"')
|
||||||
|
{
|
||||||
|
builder.Append('\\', (backslashCount * 2) + 1);
|
||||||
|
builder.Append('"');
|
||||||
|
backslashCount = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backslashCount > 0)
|
||||||
|
{
|
||||||
|
builder.Append('\\', backslashCount);
|
||||||
|
backslashCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backslashCount > 0)
|
||||||
|
{
|
||||||
|
builder.Append('\\', backslashCount * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append('"');
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PrintUsage()
|
||||||
|
{
|
||||||
|
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--log-level=<level>] <path-to-eboot.bin>");
|
||||||
|
Log.Info(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug ""E:\Games\...\eboot.bin""");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseArguments(
|
||||||
|
string[] args,
|
||||||
|
out string ebootPath,
|
||||||
|
out SharpEmuRuntimeOptions runtimeOptions,
|
||||||
|
out LogLevel logLevel)
|
||||||
|
{
|
||||||
|
if (args.Length == 0)
|
||||||
|
{
|
||||||
|
ebootPath = string.Empty;
|
||||||
|
runtimeOptions = default;
|
||||||
|
logLevel = SharpEmuLog.MinimumLevel;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var strictDynlibResolution = false;
|
||||||
|
var importTraceLimit = 0;
|
||||||
|
var cpuEngine = CpuExecutionEngine.NativeOnly;
|
||||||
|
logLevel = SharpEmuLog.MinimumLevel;
|
||||||
|
var pathTokens = new List<string>(args.Length);
|
||||||
|
for (var i = 0; i < args.Length; i++)
|
||||||
|
{
|
||||||
|
var argument = args[i];
|
||||||
|
if (string.Equals(argument, "--strict", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
strictDynlibResolution = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(argument, "--trace-imports", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
importTraceLimit = DefaultImportTraceLimit;
|
||||||
|
if (i + 1 < args.Length && int.TryParse(args[i + 1], out var explicitLimit))
|
||||||
|
{
|
||||||
|
importTraceLimit = Math.Max(0, explicitLimit);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(argument, "--log-level", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (i + 1 >= args.Length || !SharpEmuLog.TryParseLevel(args[i + 1], out logLevel))
|
||||||
|
{
|
||||||
|
ebootPath = string.Empty;
|
||||||
|
runtimeOptions = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(argument, "--cpu-engine", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (i + 1 >= args.Length || !TryParseCpuEngine(args[i + 1], out cpuEngine))
|
||||||
|
{
|
||||||
|
ebootPath = string.Empty;
|
||||||
|
runtimeOptions = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const string logLevelPrefix = "--log-level=";
|
||||||
|
if (argument.StartsWith(logLevelPrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var valueText = argument[logLevelPrefix.Length..];
|
||||||
|
if (!SharpEmuLog.TryParseLevel(valueText, out logLevel))
|
||||||
|
{
|
||||||
|
ebootPath = string.Empty;
|
||||||
|
runtimeOptions = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const string cpuEnginePrefix = "--cpu-engine=";
|
||||||
|
if (argument.StartsWith(cpuEnginePrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var valueText = argument[cpuEnginePrefix.Length..];
|
||||||
|
if (!TryParseCpuEngine(valueText, out cpuEngine))
|
||||||
|
{
|
||||||
|
ebootPath = string.Empty;
|
||||||
|
runtimeOptions = default;
|
||||||
|
logLevel = SharpEmuLog.MinimumLevel;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const string tracePrefix = "--trace-imports=";
|
||||||
|
if (argument.StartsWith(tracePrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var valueText = argument[tracePrefix.Length..];
|
||||||
|
if (!int.TryParse(valueText, out importTraceLimit))
|
||||||
|
{
|
||||||
|
ebootPath = string.Empty;
|
||||||
|
runtimeOptions = default;
|
||||||
|
logLevel = SharpEmuLog.MinimumLevel;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
importTraceLimit = Math.Max(0, importTraceLimit);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argument.StartsWith("--", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
ebootPath = string.Empty;
|
||||||
|
runtimeOptions = default;
|
||||||
|
logLevel = SharpEmuLog.MinimumLevel;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pathTokens.Add(argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathTokens.Count == 0)
|
||||||
|
{
|
||||||
|
ebootPath = string.Empty;
|
||||||
|
runtimeOptions = default;
|
||||||
|
logLevel = SharpEmuLog.MinimumLevel;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ebootPath = string.Join(' ', pathTokens);
|
||||||
|
runtimeOptions = new SharpEmuRuntimeOptions
|
||||||
|
{
|
||||||
|
CpuEngine = cpuEngine,
|
||||||
|
StrictDynlibResolution = strictDynlibResolution,
|
||||||
|
ImportTraceLimit = importTraceLimit,
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseCpuEngine(string valueText, out CpuExecutionEngine engine)
|
||||||
|
{
|
||||||
|
if (string.Equals(valueText, "native", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(valueText, "native-only", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
engine = CpuExecutionEngine.NativeOnly;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
engine = CpuExecutionEngine.NativeOnly;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||||
|
private struct STARTUPINFO
|
||||||
|
{
|
||||||
|
public int cb;
|
||||||
|
public nint lpReserved;
|
||||||
|
public nint lpDesktop;
|
||||||
|
public nint lpTitle;
|
||||||
|
public int dwX;
|
||||||
|
public int dwY;
|
||||||
|
public int dwXSize;
|
||||||
|
public int dwYSize;
|
||||||
|
public int dwXCountChars;
|
||||||
|
public int dwYCountChars;
|
||||||
|
public int dwFillAttribute;
|
||||||
|
public int dwFlags;
|
||||||
|
public short wShowWindow;
|
||||||
|
public short cbReserved2;
|
||||||
|
public nint lpReserved2;
|
||||||
|
public nint hStdInput;
|
||||||
|
public nint hStdOutput;
|
||||||
|
public nint hStdError;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct STARTUPINFOEX
|
||||||
|
{
|
||||||
|
public STARTUPINFO StartupInfo;
|
||||||
|
public nint lpAttributeList;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct PROCESS_INFORMATION
|
||||||
|
{
|
||||||
|
public nint hProcess;
|
||||||
|
public nint hThread;
|
||||||
|
public int dwProcessId;
|
||||||
|
public int dwThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct JOBOBJECT_BASIC_LIMIT_INFORMATION
|
||||||
|
{
|
||||||
|
public long PerProcessUserTimeLimit;
|
||||||
|
public long PerJobUserTimeLimit;
|
||||||
|
public uint LimitFlags;
|
||||||
|
public nuint MinimumWorkingSetSize;
|
||||||
|
public nuint MaximumWorkingSetSize;
|
||||||
|
public uint ActiveProcessLimit;
|
||||||
|
public nint Affinity;
|
||||||
|
public uint PriorityClass;
|
||||||
|
public uint SchedulingClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct IO_COUNTERS
|
||||||
|
{
|
||||||
|
public ulong ReadOperationCount;
|
||||||
|
public ulong WriteOperationCount;
|
||||||
|
public ulong OtherOperationCount;
|
||||||
|
public ulong ReadTransferCount;
|
||||||
|
public ulong WriteTransferCount;
|
||||||
|
public ulong OtherTransferCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
||||||
|
{
|
||||||
|
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
|
||||||
|
public IO_COUNTERS IoInfo;
|
||||||
|
public nuint ProcessMemoryLimit;
|
||||||
|
public nuint JobMemoryLimit;
|
||||||
|
public nuint PeakProcessMemoryUsed;
|
||||||
|
public nuint PeakJobMemoryUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool InitializeProcThreadAttributeList(
|
||||||
|
nint lpAttributeList,
|
||||||
|
int dwAttributeCount,
|
||||||
|
int dwFlags,
|
||||||
|
ref nuint lpSize);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool UpdateProcThreadAttribute(
|
||||||
|
nint lpAttributeList,
|
||||||
|
uint dwFlags,
|
||||||
|
nint attribute,
|
||||||
|
nint lpValue,
|
||||||
|
nuint cbSize,
|
||||||
|
nint lpPreviousValue,
|
||||||
|
nint lpReturnSize);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
private static extern void DeleteProcThreadAttributeList(nint lpAttributeList);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", EntryPoint = "CreateJobObjectW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
|
private static extern nint CreateJobObjectW(nint lpJobAttributes, string? lpName);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool SetInformationJobObject(
|
||||||
|
nint hJob,
|
||||||
|
int jobObjectInfoClass,
|
||||||
|
nint lpJobObjectInfo,
|
||||||
|
uint cbJobObjectInfoLength);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool AssignProcessToJobObject(nint hJob, nint hProcess);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool CreateProcessW(
|
||||||
|
string applicationName,
|
||||||
|
StringBuilder commandLine,
|
||||||
|
nint processAttributes,
|
||||||
|
nint threadAttributes,
|
||||||
|
[MarshalAs(UnmanagedType.Bool)] bool inheritHandles,
|
||||||
|
uint creationFlags,
|
||||||
|
nint environment,
|
||||||
|
string currentDirectory,
|
||||||
|
ref STARTUPINFOEX startupInfo,
|
||||||
|
out PROCESS_INFORMATION processInformation);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern uint WaitForSingleObject(nint handle, uint milliseconds);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool GetExitCodeProcess(nint process, out uint exitCode);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool TerminateProcess(nint process, uint exitCode);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool CloseHandle(nint handle);
|
||||||
|
}
|
||||||
41
src/SharpEmu.CLI/SharpEmu.CLI.csproj
Normal file
41
src/SharpEmu.CLI/SharpEmu.CLI.csproj
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SharpEmu.Core\SharpEmu.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<AssemblyName>SharpEmu</AssemblyName>
|
||||||
|
<RuntimeIdentifiers>win-x64;linux-x64;osx-arm64</RuntimeIdentifiers>
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||||
|
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
<Version>0.0.1</Version>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||||
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
|
<DebugType>none</DebugType>
|
||||||
|
<DebugSymbols>false</DebugSymbols>
|
||||||
|
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="..\..\LICENSE.txt">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
<TargetPath>LICENSE.txt</TargetPath>
|
||||||
|
<Visible>False</Visible>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
43
src/SharpEmu.CLI/packages.lock.json
Normal file
43
src/SharpEmu.CLI/packages.lock.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"dependencies": {
|
||||||
|
"net10.0": {
|
||||||
|
"Microsoft.NET.ILLink.Tasks": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[10.0.3, )",
|
||||||
|
"resolved": "10.0.3",
|
||||||
|
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
|
||||||
|
},
|
||||||
|
"sharpemu.core": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"Iced": "[1.21.0, )",
|
||||||
|
"SharpEmu.HLE": "[1.0.0, )",
|
||||||
|
"SharpEmu.Libs": "[1.0.0, )",
|
||||||
|
"SharpEmu.Logging": "[1.0.0, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sharpemu.hle": {
|
||||||
|
"type": "Project"
|
||||||
|
},
|
||||||
|
"sharpemu.libs": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"SharpEmu.HLE": "[1.0.0, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sharpemu.logging": {
|
||||||
|
"type": "Project"
|
||||||
|
},
|
||||||
|
"Iced": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[1.21.0, )",
|
||||||
|
"resolved": "1.21.0",
|
||||||
|
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"net10.0/linux-x64": {},
|
||||||
|
"net10.0/osx-arm64": {},
|
||||||
|
"net10.0/win-x64": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/SharpEmu.Core/Cpu/CpuControlTransferInfo.cs
Normal file
63
src/SharpEmu.Core/Cpu/CpuControlTransferInfo.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
|
public readonly struct CpuControlTransferInfo
|
||||||
|
{
|
||||||
|
public CpuControlTransferInfo(
|
||||||
|
ulong sourceInstructionPointer,
|
||||||
|
byte opcode,
|
||||||
|
ulong targetInstructionPointer,
|
||||||
|
CpuControlTransferKind kind,
|
||||||
|
bool isIndirect,
|
||||||
|
ulong rax,
|
||||||
|
ulong rbx,
|
||||||
|
ulong rcx,
|
||||||
|
ulong rdx,
|
||||||
|
ulong rsi,
|
||||||
|
ulong rdi,
|
||||||
|
ulong rsp,
|
||||||
|
ulong rbp)
|
||||||
|
{
|
||||||
|
SourceInstructionPointer = sourceInstructionPointer;
|
||||||
|
Opcode = opcode;
|
||||||
|
TargetInstructionPointer = targetInstructionPointer;
|
||||||
|
Kind = kind;
|
||||||
|
IsIndirect = isIndirect;
|
||||||
|
Rax = rax;
|
||||||
|
Rbx = rbx;
|
||||||
|
Rcx = rcx;
|
||||||
|
Rdx = rdx;
|
||||||
|
Rsi = rsi;
|
||||||
|
Rdi = rdi;
|
||||||
|
Rsp = rsp;
|
||||||
|
Rbp = rbp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong SourceInstructionPointer { get; }
|
||||||
|
|
||||||
|
public byte Opcode { get; }
|
||||||
|
|
||||||
|
public ulong TargetInstructionPointer { get; }
|
||||||
|
|
||||||
|
public CpuControlTransferKind Kind { get; }
|
||||||
|
|
||||||
|
public bool IsIndirect { get; }
|
||||||
|
|
||||||
|
public ulong Rax { get; }
|
||||||
|
|
||||||
|
public ulong Rbx { get; }
|
||||||
|
|
||||||
|
public ulong Rcx { get; }
|
||||||
|
|
||||||
|
public ulong Rdx { get; }
|
||||||
|
|
||||||
|
public ulong Rsi { get; }
|
||||||
|
|
||||||
|
public ulong Rdi { get; }
|
||||||
|
|
||||||
|
public ulong Rsp { get; }
|
||||||
|
|
||||||
|
public ulong Rbp { get; }
|
||||||
|
}
|
||||||
13
src/SharpEmu.Core/Cpu/CpuControlTransferKind.cs
Normal file
13
src/SharpEmu.Core/Cpu/CpuControlTransferKind.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
|
public enum CpuControlTransferKind
|
||||||
|
{
|
||||||
|
Call = 0,
|
||||||
|
|
||||||
|
Jump = 1,
|
||||||
|
|
||||||
|
Return = 2,
|
||||||
|
}
|
||||||
580
src/SharpEmu.Core/Cpu/CpuDispatcher.cs
Normal file
580
src/SharpEmu.Core/Cpu/CpuDispatcher.cs
Normal file
@@ -0,0 +1,580 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Text;
|
||||||
|
using SharpEmu.Core.Cpu.Native;
|
||||||
|
using SharpEmu.Core.Loader;
|
||||||
|
using SharpEmu.Core.Memory;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
|
public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||||
|
{
|
||||||
|
private const ulong StackBaseAddress = 0x7FFF_F000_0000UL;
|
||||||
|
private const ulong StackSize = 0x0020_0000UL;
|
||||||
|
private const ulong TlsBaseAddress = 0x7FFE_0000_0000UL;
|
||||||
|
private const ulong TlsSize = 0x0001_0000UL;
|
||||||
|
private const ulong BootstrapStubBaseAddress = 0x7FFD_F000_0000UL;
|
||||||
|
private const ulong BootstrapPayloadBaseAddress = 0x7FFD_E000_0000UL;
|
||||||
|
private const ulong DynlibFallbackStubBaseAddress = 0x7FFD_D000_0000UL;
|
||||||
|
private const ulong ReturnToHostStubBaseAddress = 0x7FFD_C000_0000UL;
|
||||||
|
private const ulong BootstrapRegionSize = 0x0000_1000UL;
|
||||||
|
private const ulong ReturnToHostStubStride = 0x0100_0000UL;
|
||||||
|
private const ulong BootstrapPayloadResultOffset = 0x28UL;
|
||||||
|
private const ulong BootstrapStatusOffset = 0x100UL;
|
||||||
|
private static readonly byte[] BootstrapStartSignature =
|
||||||
|
[
|
||||||
|
0x55, 0x48, 0x89, 0xE5, 0x41, 0x57, 0x41, 0x56,
|
||||||
|
0x41, 0x55, 0x41, 0x54, 0x53, 0x50, 0x48, 0x89,
|
||||||
|
];
|
||||||
|
private readonly IVirtualMemory _virtualMemory;
|
||||||
|
private readonly IModuleManager _moduleManager;
|
||||||
|
private INativeCpuBackend? _nativeCpuBackend;
|
||||||
|
|
||||||
|
public CpuDispatcher(
|
||||||
|
IVirtualMemory virtualMemory,
|
||||||
|
IModuleManager moduleManager,
|
||||||
|
INativeCpuBackend? nativeCpuBackend = null)
|
||||||
|
{
|
||||||
|
_virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory));
|
||||||
|
_moduleManager = moduleManager ?? throw new ArgumentNullException(nameof(moduleManager));
|
||||||
|
_nativeCpuBackend = nativeCpuBackend;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong? LastEntryPoint { get; private set; }
|
||||||
|
|
||||||
|
public CpuTrapInfo? LastTrapInfo { get; private set; }
|
||||||
|
|
||||||
|
public CpuMemoryFaultInfo? LastMemoryFaultInfo { get; private set; }
|
||||||
|
|
||||||
|
public CpuControlTransferInfo? LastControlTransferInfo { get; private set; }
|
||||||
|
|
||||||
|
public CpuNotImplementedInfo? LastNotImplementedInfo { get; private set; }
|
||||||
|
|
||||||
|
public string? LastImportResolutionTrace { get; private set; }
|
||||||
|
|
||||||
|
public string? LastBasicBlockTrace { get; private set; }
|
||||||
|
|
||||||
|
public string? LastMilestoneLog { get; private set; }
|
||||||
|
|
||||||
|
public string? LastRecentInstructionWindow { get; private set; }
|
||||||
|
|
||||||
|
public string? LastRecentControlTransferTrace { get; private set; }
|
||||||
|
|
||||||
|
public CpuSessionSummary LastSessionSummary { get; private set; }
|
||||||
|
|
||||||
|
public OrbisGen2Result DispatchEntry(
|
||||||
|
ulong entryPoint,
|
||||||
|
Generation generation,
|
||||||
|
IReadOnlyDictionary<ulong, string>? importStubs = null,
|
||||||
|
IReadOnlyDictionary<string, ulong>? runtimeSymbols = null,
|
||||||
|
string processImageName = "eboot.bin",
|
||||||
|
CpuExecutionOptions executionOptions = default)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[DISPATCHER] === DispatchEntry START ===");
|
||||||
|
Console.Error.WriteLine($"[DISPATCHER] entryPoint=0x{entryPoint:X16}, generation={generation}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return DispatchEntryCore(entryPoint, generation, importStubs, runtimeSymbols, processImageName, executionOptions);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}");
|
||||||
|
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private OrbisGen2Result DispatchEntryCore(
|
||||||
|
ulong entryPoint,
|
||||||
|
Generation generation,
|
||||||
|
IReadOnlyDictionary<ulong, string>? importStubs = null,
|
||||||
|
IReadOnlyDictionary<string, ulong>? runtimeSymbols = null,
|
||||||
|
string processImageName = "eboot.bin",
|
||||||
|
CpuExecutionOptions executionOptions = default)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[DISPATCHER] DispatchEntryCore STARTING...");
|
||||||
|
|
||||||
|
LastEntryPoint = entryPoint;
|
||||||
|
LastTrapInfo = null;
|
||||||
|
LastMemoryFaultInfo = null;
|
||||||
|
LastControlTransferInfo = null;
|
||||||
|
LastNotImplementedInfo = null;
|
||||||
|
LastImportResolutionTrace = null;
|
||||||
|
LastBasicBlockTrace = null;
|
||||||
|
LastMilestoneLog = null;
|
||||||
|
LastRecentInstructionWindow = null;
|
||||||
|
LastRecentControlTransferTrace = null;
|
||||||
|
LastSessionSummary = default;
|
||||||
|
OrbisGen2Result FailEarly(OrbisGen2Result result, CpuExitReason reason = CpuExitReason.UnhandledException)
|
||||||
|
{
|
||||||
|
LastSessionSummary = new CpuSessionSummary(
|
||||||
|
result,
|
||||||
|
reason,
|
||||||
|
exitCode: null,
|
||||||
|
lastGuestRip: entryPoint,
|
||||||
|
lastStubRip: 0,
|
||||||
|
totalInstructions: 0,
|
||||||
|
importsHit: 0,
|
||||||
|
uniqueNidsHit: 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stackBase = TryMapStackRegion();
|
||||||
|
if (stackBase == 0)
|
||||||
|
{
|
||||||
|
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var tlsBase = TryMapTlsRegion();
|
||||||
|
if (tlsBase == 0)
|
||||||
|
{
|
||||||
|
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var trackedMemory = new TrackedCpuMemory(_virtualMemory);
|
||||||
|
var context = new CpuContext(trackedMemory, generation)
|
||||||
|
{
|
||||||
|
Rip = entryPoint,
|
||||||
|
Rflags = 0x202,
|
||||||
|
FsBase = tlsBase,
|
||||||
|
GsBase = tlsBase,
|
||||||
|
};
|
||||||
|
|
||||||
|
var returnToHostStubAddress = TryMapReturnToHostStubRegion();
|
||||||
|
if (returnToHostStubAddress == 0)
|
||||||
|
{
|
||||||
|
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
context[CpuRegister.Rsp] = stackBase + StackSize - sizeof(ulong);
|
||||||
|
if (!context.TryWriteUInt64(context[CpuRegister.Rsp], returnToHostStubAddress))
|
||||||
|
{
|
||||||
|
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!InitializeTls(context, tlsBase))
|
||||||
|
{
|
||||||
|
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var effectiveImportStubs = importStubs is null
|
||||||
|
? new Dictionary<ulong, string>()
|
||||||
|
: new Dictionary<ulong, string>(importStubs);
|
||||||
|
var programExitHandlerStubAddress = TryMapDynlibFallbackStubRegion();
|
||||||
|
if (programExitHandlerStubAddress == 0)
|
||||||
|
{
|
||||||
|
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!InitializeProcessEntryFrame(context, processImageName, programExitHandlerStubAddress))
|
||||||
|
{
|
||||||
|
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ShouldInjectBootstrapPayload(entryPoint))
|
||||||
|
{
|
||||||
|
if (!TryInstallBootstrapPayload(context, effectiveImportStubs))
|
||||||
|
{
|
||||||
|
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var entryFrameDiagnostic = BuildEntryFrameDiagnostic(
|
||||||
|
entryPoint,
|
||||||
|
context,
|
||||||
|
sentinelEnabled: true,
|
||||||
|
sentinelValue: returnToHostStubAddress,
|
||||||
|
entryParamsConfigured: true);
|
||||||
|
|
||||||
|
if (executionOptions.CpuEngine != CpuExecutionEngine.NativeOnly)
|
||||||
|
{
|
||||||
|
LastMilestoneLog = string.Concat(
|
||||||
|
entryFrameDiagnostic,
|
||||||
|
Environment.NewLine,
|
||||||
|
$"CpuEngine: {executionOptions.CpuEngine} (unsupported)");
|
||||||
|
LastNotImplementedInfo = new CpuNotImplementedInfo(
|
||||||
|
CpuNotImplementedSource.NativeBackend,
|
||||||
|
entryPoint,
|
||||||
|
nid: null,
|
||||||
|
exportName: "cpu_engine_unsupported",
|
||||||
|
libraryName: executionOptions.CpuEngine.ToString(),
|
||||||
|
detail: "Unsupported CPU engine mode.");
|
||||||
|
return FailEarly(
|
||||||
|
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
|
||||||
|
CpuExitReason.NativeBackendUnavailable);
|
||||||
|
}
|
||||||
|
|
||||||
|
LastMilestoneLog = string.Concat(
|
||||||
|
entryFrameDiagnostic,
|
||||||
|
Environment.NewLine,
|
||||||
|
"CpuEngine: native-only");
|
||||||
|
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager);
|
||||||
|
if (_nativeCpuBackend.TryExecute(
|
||||||
|
context,
|
||||||
|
entryPoint,
|
||||||
|
generation,
|
||||||
|
effectiveImportStubs,
|
||||||
|
runtimeSymbols ?? new Dictionary<string, ulong>(StringComparer.Ordinal),
|
||||||
|
executionOptions,
|
||||||
|
out var nativeResult))
|
||||||
|
{
|
||||||
|
LastSessionSummary = new CpuSessionSummary(
|
||||||
|
nativeResult,
|
||||||
|
nativeResult == OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
? CpuExitReason.ReturnedToHost
|
||||||
|
: CpuExitReason.UnhandledException,
|
||||||
|
exitCode: null,
|
||||||
|
lastGuestRip: context.Rip,
|
||||||
|
lastStubRip: 0,
|
||||||
|
totalInstructions: 0,
|
||||||
|
importsHit: 0,
|
||||||
|
uniqueNidsHit: 0);
|
||||||
|
return nativeResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
var backendName = string.IsNullOrWhiteSpace(_nativeCpuBackend.BackendName)
|
||||||
|
? "native-backend"
|
||||||
|
: _nativeCpuBackend.BackendName;
|
||||||
|
var backendError = string.IsNullOrWhiteSpace(_nativeCpuBackend.LastError)
|
||||||
|
? "unknown backend error"
|
||||||
|
: _nativeCpuBackend.LastError;
|
||||||
|
LastNotImplementedInfo = new CpuNotImplementedInfo(
|
||||||
|
CpuNotImplementedSource.NativeBackend,
|
||||||
|
entryPoint,
|
||||||
|
nid: null,
|
||||||
|
exportName: "cpu_engine_native_only",
|
||||||
|
libraryName: backendName,
|
||||||
|
detail: backendError);
|
||||||
|
LastMilestoneLog = string.Concat(
|
||||||
|
LastMilestoneLog,
|
||||||
|
Environment.NewLine,
|
||||||
|
$"CpuEngine native-only failed: {backendError}");
|
||||||
|
Console.Error.WriteLine($"[DISPATCHER] Native backend FAILED: {backendError}");
|
||||||
|
return FailEarly(
|
||||||
|
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
|
||||||
|
CpuExitReason.NativeBackendUnavailable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ulong TryMapStackRegion()
|
||||||
|
{
|
||||||
|
const ulong stackStride = 0x0100_0000UL;
|
||||||
|
for (var i = 0; i < 32; i++)
|
||||||
|
{
|
||||||
|
var candidateBase = StackBaseAddress - ((ulong)i * stackStride);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_virtualMemory.Map(
|
||||||
|
candidateBase,
|
||||||
|
StackSize,
|
||||||
|
fileOffset: 0,
|
||||||
|
fileData: ReadOnlySpan<byte>.Empty,
|
||||||
|
ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
|
||||||
|
return candidateBase;
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ulong TryMapTlsRegion()
|
||||||
|
{
|
||||||
|
const ulong tlsStride = 0x0100_0000UL;
|
||||||
|
for (var i = 0; i < 32; i++)
|
||||||
|
{
|
||||||
|
var candidateBase = TlsBaseAddress - ((ulong)i * tlsStride);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_virtualMemory.Map(
|
||||||
|
candidateBase,
|
||||||
|
TlsSize,
|
||||||
|
fileOffset: 0,
|
||||||
|
fileData: ReadOnlySpan<byte>.Empty,
|
||||||
|
ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
|
||||||
|
return candidateBase;
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool InitializeTls(CpuContext context, ulong tlsBase)
|
||||||
|
{
|
||||||
|
return context.TryWriteUInt64(tlsBase + 0x00, tlsBase) &&
|
||||||
|
context.TryWriteUInt64(tlsBase + 0x10, tlsBase) &&
|
||||||
|
context.TryWriteUInt64(tlsBase + 0x28, 0xC0DEC0DECAFEBABEUL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool InitializeProcessEntryFrame(
|
||||||
|
CpuContext context,
|
||||||
|
string processImageName,
|
||||||
|
ulong programExitHandlerAddress)
|
||||||
|
{
|
||||||
|
var imageName = string.IsNullOrWhiteSpace(processImageName) ? "eboot.bin" : processImageName;
|
||||||
|
var encodedNameLength = Encoding.UTF8.GetByteCount(imageName);
|
||||||
|
Span<byte> argv0Buffer = encodedNameLength + 1 <= 512
|
||||||
|
? stackalloc byte[encodedNameLength + 1]
|
||||||
|
: new byte[encodedNameLength + 1];
|
||||||
|
if (Encoding.UTF8.GetBytes(imageName.AsSpan(), argv0Buffer) != encodedNameLength)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
argv0Buffer[encodedNameLength] = 0;
|
||||||
|
var cursor = context[CpuRegister.Rsp];
|
||||||
|
|
||||||
|
var argv0Address = AlignDown(cursor - (ulong)argv0Buffer.Length, 16);
|
||||||
|
if (!context.Memory.TryWrite(argv0Address, argv0Buffer))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ulong entryParamsSize = 0x20;
|
||||||
|
var entryParamsAddress = AlignDown(argv0Address - entryParamsSize, 16);
|
||||||
|
if (!TryWriteUInt32(context, entryParamsAddress + 0x00, 1) ||
|
||||||
|
!TryWriteUInt32(context, entryParamsAddress + 0x04, 0) ||
|
||||||
|
!context.TryWriteUInt64(entryParamsAddress + 0x08, argv0Address) ||
|
||||||
|
!context.TryWriteUInt64(entryParamsAddress + 0x10, 0) ||
|
||||||
|
!context.TryWriteUInt64(entryParamsAddress + 0x18, 0))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
context[CpuRegister.Rdi] = entryParamsAddress;
|
||||||
|
context[CpuRegister.Rsi] = programExitHandlerAddress;
|
||||||
|
context[CpuRegister.Rdx] = 0;
|
||||||
|
context[CpuRegister.Rcx] = 0;
|
||||||
|
context[CpuRegister.R8] = 0;
|
||||||
|
context[CpuRegister.R9] = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong AlignDown(ulong value, ulong alignment)
|
||||||
|
{
|
||||||
|
return value & ~(alignment - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext context, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
|
return context.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildEntryFrameDiagnostic(
|
||||||
|
ulong entryPoint,
|
||||||
|
CpuContext context,
|
||||||
|
bool sentinelEnabled,
|
||||||
|
ulong sentinelValue,
|
||||||
|
bool entryParamsConfigured)
|
||||||
|
{
|
||||||
|
var initialRsp = context[CpuRegister.Rsp];
|
||||||
|
var stackTopText = context.TryReadUInt64(initialRsp, out var stackTop)
|
||||||
|
? $"0x{stackTop:X16}"
|
||||||
|
: "??";
|
||||||
|
var sentinelText = sentinelEnabled ? "yes" : "no";
|
||||||
|
var sentinelValueText = sentinelEnabled ? $"0x{sentinelValue:X16}" : "??";
|
||||||
|
var entryParamsText = entryParamsConfigured ? "yes" : "no";
|
||||||
|
return
|
||||||
|
$"EntryFrame: entry_rip=0x{entryPoint:X16} initial_rsp=0x{initialRsp:X16} [rsp]={stackTopText} sentinel_enabled={sentinelText} sentinel_value={sentinelValueText} entry_params_configured={entryParamsText}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ShouldInjectBootstrapPayload(ulong entryPoint)
|
||||||
|
{
|
||||||
|
Span<byte> probe = stackalloc byte[16];
|
||||||
|
if (!_virtualMemory.TryRead(entryPoint, probe))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < BootstrapStartSignature.Length; i++)
|
||||||
|
{
|
||||||
|
if (probe[i] != BootstrapStartSignature[i])
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryInstallBootstrapPayload(CpuContext context, IDictionary<ulong, string> importStubs)
|
||||||
|
{
|
||||||
|
var stubAddress = TryMapBootstrapStubRegion();
|
||||||
|
if (stubAddress == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var payloadAddress = TryMapBootstrapPayloadRegion();
|
||||||
|
if (payloadAddress == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusAddress = payloadAddress + BootstrapStatusOffset;
|
||||||
|
if (!TryWriteUInt64(payloadAddress, stubAddress) ||
|
||||||
|
!TryWriteUInt64(payloadAddress + 0x08, statusAddress) ||
|
||||||
|
!TryWriteUInt64(payloadAddress + 0x10, statusAddress) ||
|
||||||
|
!TryWriteUInt64(payloadAddress + 0x18, statusAddress) ||
|
||||||
|
!TryWriteUInt64(payloadAddress + 0x20, statusAddress) ||
|
||||||
|
!TryWriteUInt64(payloadAddress + BootstrapPayloadResultOffset, statusAddress) ||
|
||||||
|
!TryWriteUInt32(statusAddress, 0))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
importStubs[stubAddress] = RuntimeStubNids.BootstrapBridge;
|
||||||
|
importStubs[stubAddress + 0x0A] = RuntimeStubNids.BootstrapBridge;
|
||||||
|
|
||||||
|
context[CpuRegister.Rdi] = payloadAddress;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ulong TryMapBootstrapStubRegion()
|
||||||
|
{
|
||||||
|
var stubData = new byte[(int)BootstrapRegionSize];
|
||||||
|
stubData[0] = 0xCC;
|
||||||
|
stubData[1] = 0xC3;
|
||||||
|
|
||||||
|
const ulong stride = 0x0100_0000UL;
|
||||||
|
for (var i = 0; i < 16; i++)
|
||||||
|
{
|
||||||
|
var candidateBase = BootstrapStubBaseAddress - ((ulong)i * stride);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_virtualMemory.Map(
|
||||||
|
candidateBase,
|
||||||
|
BootstrapRegionSize,
|
||||||
|
fileOffset: 0,
|
||||||
|
stubData,
|
||||||
|
ProgramHeaderFlags.Read | ProgramHeaderFlags.Execute);
|
||||||
|
return candidateBase;
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ulong TryMapBootstrapPayloadRegion()
|
||||||
|
{
|
||||||
|
const ulong stride = 0x0100_0000UL;
|
||||||
|
for (var i = 0; i < 16; i++)
|
||||||
|
{
|
||||||
|
var candidateBase = BootstrapPayloadBaseAddress - ((ulong)i * stride);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_virtualMemory.Map(
|
||||||
|
candidateBase,
|
||||||
|
BootstrapRegionSize,
|
||||||
|
fileOffset: 0,
|
||||||
|
ReadOnlySpan<byte>.Empty,
|
||||||
|
ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
|
||||||
|
return candidateBase;
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ulong TryMapDynlibFallbackStubRegion()
|
||||||
|
{
|
||||||
|
var stubData = new byte[(int)BootstrapRegionSize];
|
||||||
|
stubData[0] = 0x31;
|
||||||
|
stubData[1] = 0xC0;
|
||||||
|
stubData[2] = 0xC3;
|
||||||
|
|
||||||
|
const ulong stride = 0x0100_0000UL;
|
||||||
|
for (var i = 0; i < 16; i++)
|
||||||
|
{
|
||||||
|
var candidateBase = DynlibFallbackStubBaseAddress - ((ulong)i * stride);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_virtualMemory.Map(
|
||||||
|
candidateBase,
|
||||||
|
BootstrapRegionSize,
|
||||||
|
fileOffset: 0,
|
||||||
|
stubData,
|
||||||
|
ProgramHeaderFlags.Read | ProgramHeaderFlags.Execute);
|
||||||
|
return candidateBase;
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ulong TryMapReturnToHostStubRegion()
|
||||||
|
{
|
||||||
|
var stubData = new byte[(int)BootstrapRegionSize];
|
||||||
|
stubData[0] = 0xF4;
|
||||||
|
stubData[1] = 0xCC;
|
||||||
|
|
||||||
|
for (var i = 0; i < 16; i++)
|
||||||
|
{
|
||||||
|
var candidateBase = ReturnToHostStubBaseAddress - ((ulong)i * ReturnToHostStubStride);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_virtualMemory.Map(
|
||||||
|
candidateBase,
|
||||||
|
BootstrapRegionSize,
|
||||||
|
fileOffset: 0,
|
||||||
|
stubData,
|
||||||
|
ProgramHeaderFlags.Read | ProgramHeaderFlags.Execute);
|
||||||
|
return candidateBase;
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryWriteUInt64(ulong address, ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||||
|
return _virtualMemory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryWriteUInt32(ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
|
return _virtualMemory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_nativeCpuBackend is IDisposable disposableBackend)
|
||||||
|
{
|
||||||
|
disposableBackend.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_nativeCpuBackend = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/SharpEmu.Core/Cpu/CpuExecutionEngine.cs
Normal file
9
src/SharpEmu.Core/Cpu/CpuExecutionEngine.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
|
public enum CpuExecutionEngine
|
||||||
|
{
|
||||||
|
NativeOnly = 0,
|
||||||
|
}
|
||||||
15
src/SharpEmu.Core/Cpu/CpuExecutionOptions.cs
Normal file
15
src/SharpEmu.Core/Cpu/CpuExecutionOptions.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
|
public readonly struct CpuExecutionOptions
|
||||||
|
{
|
||||||
|
public bool EnableDisasmDiagnostics { get; init; }
|
||||||
|
|
||||||
|
public CpuExecutionEngine CpuEngine { get; init; }
|
||||||
|
|
||||||
|
public bool StrictDynlibResolution { get; init; }
|
||||||
|
|
||||||
|
public int ImportTraceLimit { get; init; }
|
||||||
|
}
|
||||||
25
src/SharpEmu.Core/Cpu/CpuExitReason.cs
Normal file
25
src/SharpEmu.Core/Cpu/CpuExitReason.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
|
public enum CpuExitReason
|
||||||
|
{
|
||||||
|
Exited = 0,
|
||||||
|
|
||||||
|
SentinelReturn = 1,
|
||||||
|
|
||||||
|
ReturnedToHost = 2,
|
||||||
|
|
||||||
|
Halted = 3,
|
||||||
|
|
||||||
|
BudgetExceeded = 4,
|
||||||
|
|
||||||
|
CpuTrap = 5,
|
||||||
|
|
||||||
|
UnhandledException = 6,
|
||||||
|
|
||||||
|
UnhandledSyscall = 7,
|
||||||
|
|
||||||
|
NativeBackendUnavailable = 8,
|
||||||
|
}
|
||||||
20
src/SharpEmu.Core/Cpu/CpuMemoryAccessFailure.cs
Normal file
20
src/SharpEmu.Core/Cpu/CpuMemoryAccessFailure.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
|
public readonly struct CpuMemoryAccessFailure
|
||||||
|
{
|
||||||
|
public CpuMemoryAccessFailure(ulong address, int size, bool isWrite)
|
||||||
|
{
|
||||||
|
Address = address;
|
||||||
|
Size = size;
|
||||||
|
IsWrite = isWrite;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong Address { get; }
|
||||||
|
|
||||||
|
public int Size { get; }
|
||||||
|
|
||||||
|
public bool IsWrite { get; }
|
||||||
|
}
|
||||||
20
src/SharpEmu.Core/Cpu/CpuMemoryFaultInfo.cs
Normal file
20
src/SharpEmu.Core/Cpu/CpuMemoryFaultInfo.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
|
public readonly struct CpuMemoryFaultInfo
|
||||||
|
{
|
||||||
|
public CpuMemoryFaultInfo(ulong instructionPointer, byte? opcode, CpuMemoryAccessFailure access)
|
||||||
|
{
|
||||||
|
InstructionPointer = instructionPointer;
|
||||||
|
Opcode = opcode;
|
||||||
|
Access = access;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong InstructionPointer { get; }
|
||||||
|
|
||||||
|
public byte? Opcode { get; }
|
||||||
|
|
||||||
|
public CpuMemoryAccessFailure Access { get; }
|
||||||
|
}
|
||||||
35
src/SharpEmu.Core/Cpu/CpuNotImplementedInfo.cs
Normal file
35
src/SharpEmu.Core/Cpu/CpuNotImplementedInfo.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
|
public readonly struct CpuNotImplementedInfo
|
||||||
|
{
|
||||||
|
public CpuNotImplementedInfo(
|
||||||
|
CpuNotImplementedSource source,
|
||||||
|
ulong instructionPointer,
|
||||||
|
string? nid,
|
||||||
|
string? exportName,
|
||||||
|
string? libraryName,
|
||||||
|
string? detail)
|
||||||
|
{
|
||||||
|
Source = source;
|
||||||
|
InstructionPointer = instructionPointer;
|
||||||
|
Nid = nid;
|
||||||
|
ExportName = exportName;
|
||||||
|
LibraryName = libraryName;
|
||||||
|
Detail = detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CpuNotImplementedSource Source { get; }
|
||||||
|
|
||||||
|
public ulong InstructionPointer { get; }
|
||||||
|
|
||||||
|
public string? Nid { get; }
|
||||||
|
|
||||||
|
public string? ExportName { get; }
|
||||||
|
|
||||||
|
public string? LibraryName { get; }
|
||||||
|
|
||||||
|
public string? Detail { get; }
|
||||||
|
}
|
||||||
17
src/SharpEmu.Core/Cpu/CpuNotImplementedSource.cs
Normal file
17
src/SharpEmu.Core/Cpu/CpuNotImplementedSource.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
|
public enum CpuNotImplementedSource
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
|
||||||
|
InstructionBudget = 1,
|
||||||
|
|
||||||
|
KernelDynlibDlsym = 2,
|
||||||
|
|
||||||
|
HleExport = 3,
|
||||||
|
|
||||||
|
NativeBackend = 4,
|
||||||
|
}
|
||||||
45
src/SharpEmu.Core/Cpu/CpuSessionSummary.cs
Normal file
45
src/SharpEmu.Core/Cpu/CpuSessionSummary.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
|
public readonly struct CpuSessionSummary
|
||||||
|
{
|
||||||
|
public CpuSessionSummary(
|
||||||
|
OrbisGen2Result result,
|
||||||
|
CpuExitReason reason,
|
||||||
|
int? exitCode,
|
||||||
|
ulong lastGuestRip,
|
||||||
|
ulong lastStubRip,
|
||||||
|
int totalInstructions,
|
||||||
|
int importsHit,
|
||||||
|
int uniqueNidsHit)
|
||||||
|
{
|
||||||
|
Result = result;
|
||||||
|
Reason = reason;
|
||||||
|
ExitCode = exitCode;
|
||||||
|
LastGuestRip = lastGuestRip;
|
||||||
|
LastStubRip = lastStubRip;
|
||||||
|
TotalInstructions = totalInstructions;
|
||||||
|
ImportsHit = importsHit;
|
||||||
|
UniqueNidsHit = uniqueNidsHit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OrbisGen2Result Result { get; }
|
||||||
|
|
||||||
|
public CpuExitReason Reason { get; }
|
||||||
|
|
||||||
|
public int? ExitCode { get; }
|
||||||
|
|
||||||
|
public ulong LastGuestRip { get; }
|
||||||
|
|
||||||
|
public ulong LastStubRip { get; }
|
||||||
|
|
||||||
|
public int TotalInstructions { get; }
|
||||||
|
|
||||||
|
public int ImportsHit { get; }
|
||||||
|
|
||||||
|
public int UniqueNidsHit { get; }
|
||||||
|
}
|
||||||
17
src/SharpEmu.Core/Cpu/CpuTrapInfo.cs
Normal file
17
src/SharpEmu.Core/Cpu/CpuTrapInfo.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
|
public readonly struct CpuTrapInfo
|
||||||
|
{
|
||||||
|
public CpuTrapInfo(ulong instructionPointer, byte opcode)
|
||||||
|
{
|
||||||
|
InstructionPointer = instructionPointer;
|
||||||
|
Opcode = opcode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong InstructionPointer { get; }
|
||||||
|
|
||||||
|
public byte Opcode { get; }
|
||||||
|
}
|
||||||
45
src/SharpEmu.Core/Cpu/Disasm/DecodedInst.cs
Normal file
45
src/SharpEmu.Core/Cpu/Disasm/DecodedInst.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using Iced.Intel;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu.Disasm;
|
||||||
|
|
||||||
|
public readonly struct DecodedInst
|
||||||
|
{
|
||||||
|
public DecodedInst(
|
||||||
|
ulong rip,
|
||||||
|
int length,
|
||||||
|
string text,
|
||||||
|
string mnemonic,
|
||||||
|
FlowControl flowControl,
|
||||||
|
ulong? nearBranchTarget,
|
||||||
|
ulong? memoryAddress,
|
||||||
|
byte[] bytes)
|
||||||
|
{
|
||||||
|
Rip = rip;
|
||||||
|
Length = length;
|
||||||
|
Text = text;
|
||||||
|
Mnemonic = mnemonic;
|
||||||
|
FlowControl = flowControl;
|
||||||
|
NearBranchTarget = nearBranchTarget;
|
||||||
|
MemoryAddress = memoryAddress;
|
||||||
|
Bytes = bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong Rip { get; }
|
||||||
|
|
||||||
|
public int Length { get; }
|
||||||
|
|
||||||
|
public string Text { get; }
|
||||||
|
|
||||||
|
public string Mnemonic { get; }
|
||||||
|
|
||||||
|
public FlowControl FlowControl { get; }
|
||||||
|
|
||||||
|
public ulong? NearBranchTarget { get; }
|
||||||
|
|
||||||
|
public ulong? MemoryAddress { get; }
|
||||||
|
|
||||||
|
public byte[] Bytes { get; }
|
||||||
|
}
|
||||||
213
src/SharpEmu.Core/Cpu/Disasm/IcedDecoder.cs
Normal file
213
src/SharpEmu.Core/Cpu/Disasm/IcedDecoder.cs
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using Iced.Intel;
|
||||||
|
using SharpEmu.Core.Memory;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu.Disasm;
|
||||||
|
|
||||||
|
public static class IcedDecoder
|
||||||
|
{
|
||||||
|
private const int MaxInstructionBytes = 15;
|
||||||
|
|
||||||
|
public static bool TryDecode(ulong rip, ReadOnlySpan<byte> codeBytes, out DecodedInst inst)
|
||||||
|
{
|
||||||
|
if (codeBytes.IsEmpty)
|
||||||
|
{
|
||||||
|
inst = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var decodeLength = Math.Min(MaxInstructionBytes, codeBytes.Length);
|
||||||
|
var decodeBytes = GC.AllocateUninitializedArray<byte>(decodeLength);
|
||||||
|
codeBytes[..decodeLength].CopyTo(decodeBytes);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var decoder = Decoder.Create(64, new ByteArrayCodeReader(decodeBytes));
|
||||||
|
decoder.IP = rip;
|
||||||
|
decoder.Decode(out var instruction);
|
||||||
|
if (instruction.Code == Code.INVALID || instruction.Length <= 0)
|
||||||
|
{
|
||||||
|
inst = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var formatter = new IntelFormatter();
|
||||||
|
var output = new StringOutput();
|
||||||
|
formatter.Format(instruction, output);
|
||||||
|
|
||||||
|
var effectiveLength = Math.Min(instruction.Length, decodeBytes.Length);
|
||||||
|
var effectiveBytes = new byte[effectiveLength];
|
||||||
|
Array.Copy(decodeBytes, 0, effectiveBytes, 0, effectiveLength);
|
||||||
|
|
||||||
|
inst = new DecodedInst(
|
||||||
|
rip,
|
||||||
|
instruction.Length,
|
||||||
|
output.ToString(),
|
||||||
|
instruction.Mnemonic.ToString(),
|
||||||
|
instruction.FlowControl,
|
||||||
|
GetNearBranchTarget(in instruction),
|
||||||
|
GetMemoryAddress(in instruction),
|
||||||
|
effectiveBytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
inst = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryReadGuestBytes(ICpuMemory memory, ulong rip, int maxLen, out byte[] bytes)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(memory);
|
||||||
|
var clampedLength = Math.Clamp(maxLen, 1, MaxInstructionBytes);
|
||||||
|
var buffer = new byte[clampedLength];
|
||||||
|
Span<byte> oneByte = stackalloc byte[1];
|
||||||
|
var readCount = 0;
|
||||||
|
for (var i = 0; i < clampedLength; i++)
|
||||||
|
{
|
||||||
|
if (!memory.TryRead(rip + (ulong)i, oneByte))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer[readCount] = oneByte[0];
|
||||||
|
readCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readCount == 0)
|
||||||
|
{
|
||||||
|
bytes = Array.Empty<byte>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readCount == clampedLength)
|
||||||
|
{
|
||||||
|
bytes = buffer;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes = new byte[readCount];
|
||||||
|
Array.Copy(buffer, bytes, readCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryReadGuestBytes(IVirtualMemory memory, ulong rip, int maxLen, out byte[] bytes)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(memory);
|
||||||
|
var clampedLength = Math.Clamp(maxLen, 1, MaxInstructionBytes);
|
||||||
|
var buffer = new byte[clampedLength];
|
||||||
|
Span<byte> oneByte = stackalloc byte[1];
|
||||||
|
var readCount = 0;
|
||||||
|
for (var i = 0; i < clampedLength; i++)
|
||||||
|
{
|
||||||
|
if (!memory.TryRead(rip + (ulong)i, oneByte))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer[readCount] = oneByte[0];
|
||||||
|
readCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readCount == 0)
|
||||||
|
{
|
||||||
|
bytes = Array.Empty<byte>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readCount == clampedLength)
|
||||||
|
{
|
||||||
|
bytes = buffer;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes = new byte[readCount];
|
||||||
|
Array.Copy(buffer, bytes, readCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string FormatBytes(ReadOnlySpan<byte> bytes)
|
||||||
|
{
|
||||||
|
if (bytes.IsEmpty)
|
||||||
|
{
|
||||||
|
return "??";
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = new string[bytes.Length];
|
||||||
|
for (var i = 0; i < bytes.Length; i++)
|
||||||
|
{
|
||||||
|
parts[i] = bytes[i].ToString("X2");
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join(' ', parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong? GetNearBranchTarget(in Instruction instruction)
|
||||||
|
{
|
||||||
|
for (var opIndex = 0; opIndex < instruction.OpCount; opIndex++)
|
||||||
|
{
|
||||||
|
switch (instruction.GetOpKind(opIndex))
|
||||||
|
{
|
||||||
|
case OpKind.NearBranch16:
|
||||||
|
return instruction.NearBranch16;
|
||||||
|
case OpKind.NearBranch32:
|
||||||
|
return instruction.NearBranch32;
|
||||||
|
case OpKind.NearBranch64:
|
||||||
|
return instruction.NearBranch64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong? GetMemoryAddress(in Instruction instruction)
|
||||||
|
{
|
||||||
|
var hasMemoryOperand = false;
|
||||||
|
for (var opIndex = 0; opIndex < instruction.OpCount; opIndex++)
|
||||||
|
{
|
||||||
|
if (IsMemoryOpKind(instruction.GetOpKind(opIndex)))
|
||||||
|
{
|
||||||
|
hasMemoryOperand = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasMemoryOperand)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instruction.IsIPRelativeMemoryOperand)
|
||||||
|
{
|
||||||
|
return instruction.IPRelativeMemoryAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instruction.MemoryBase == Register.None &&
|
||||||
|
instruction.MemoryIndex == Register.None &&
|
||||||
|
instruction.MemoryDisplacement64 != 0)
|
||||||
|
{
|
||||||
|
return instruction.MemoryDisplacement64;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsMemoryOpKind(OpKind opKind)
|
||||||
|
{
|
||||||
|
return opKind is
|
||||||
|
OpKind.Memory or
|
||||||
|
OpKind.MemorySegSI or
|
||||||
|
OpKind.MemorySegESI or
|
||||||
|
OpKind.MemorySegRSI or
|
||||||
|
OpKind.MemorySegDI or
|
||||||
|
OpKind.MemorySegEDI or
|
||||||
|
OpKind.MemorySegRDI or
|
||||||
|
OpKind.MemoryESDI or
|
||||||
|
OpKind.MemoryESEDI or
|
||||||
|
OpKind.MemoryESRDI;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/SharpEmu.Core/Cpu/ICpuDispatcher.cs
Normal file
37
src/SharpEmu.Core/Cpu/ICpuDispatcher.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
|
public interface ICpuDispatcher
|
||||||
|
{
|
||||||
|
CpuTrapInfo? LastTrapInfo { get; }
|
||||||
|
|
||||||
|
CpuMemoryFaultInfo? LastMemoryFaultInfo { get; }
|
||||||
|
|
||||||
|
CpuControlTransferInfo? LastControlTransferInfo { get; }
|
||||||
|
|
||||||
|
CpuNotImplementedInfo? LastNotImplementedInfo { get; }
|
||||||
|
|
||||||
|
string? LastImportResolutionTrace { get; }
|
||||||
|
|
||||||
|
string? LastBasicBlockTrace { get; }
|
||||||
|
|
||||||
|
string? LastMilestoneLog { get; }
|
||||||
|
|
||||||
|
string? LastRecentInstructionWindow { get; }
|
||||||
|
|
||||||
|
string? LastRecentControlTransferTrace { get; }
|
||||||
|
|
||||||
|
CpuSessionSummary LastSessionSummary { get; }
|
||||||
|
|
||||||
|
OrbisGen2Result DispatchEntry(
|
||||||
|
ulong entryPoint,
|
||||||
|
Generation generation,
|
||||||
|
IReadOnlyDictionary<ulong, string>? importStubs = null,
|
||||||
|
IReadOnlyDictionary<string, ulong>? runtimeSymbols = null,
|
||||||
|
string processImageName = "eboot.bin",
|
||||||
|
CpuExecutionOptions executionOptions = default);
|
||||||
|
}
|
||||||
9
src/SharpEmu.Core/Cpu/ITrackedCpuMemory.cs
Normal file
9
src/SharpEmu.Core/Cpu/ITrackedCpuMemory.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
|
public interface ITrackedCpuMemory
|
||||||
|
{
|
||||||
|
CpuMemoryAccessFailure? LastFailure { get; }
|
||||||
|
}
|
||||||
47
src/SharpEmu.Core/Cpu/Native/CpuPatcher.cs
Normal file
47
src/SharpEmu.Core/Cpu/Native/CpuPatcher.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
|
public sealed unsafe class CpuPatcher : IDisposable
|
||||||
|
{
|
||||||
|
private readonly nint _tlsBaseAddress;
|
||||||
|
|
||||||
|
public CpuPatcher(nint tlsBaseAddress)
|
||||||
|
{
|
||||||
|
_tlsBaseAddress = tlsBaseAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryPatchInstruction(nint address)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
internal unsafe class UnsafeCodeReader : Iced.Intel.CodeReader
|
||||||
|
{
|
||||||
|
private byte* _start;
|
||||||
|
private byte* _current;
|
||||||
|
private int _length;
|
||||||
|
|
||||||
|
public UnsafeCodeReader(byte* start, int length)
|
||||||
|
{
|
||||||
|
_start = start;
|
||||||
|
_current = start;
|
||||||
|
_length = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int ReadByte()
|
||||||
|
{
|
||||||
|
if (_current >= _start + _length)
|
||||||
|
return -1;
|
||||||
|
return *_current++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
|
public sealed partial class DirectExecutionBackend
|
||||||
|
{
|
||||||
|
|
||||||
|
private void RecordRecentImportTrace(string traceLine)
|
||||||
|
{
|
||||||
|
_recentImportTrace[_recentImportTraceWriteIndex] = traceLine;
|
||||||
|
_recentImportTraceWriteIndex = (_recentImportTraceWriteIndex + 1) % _recentImportTrace.Length;
|
||||||
|
if (_recentImportTraceCount < _recentImportTrace.Length)
|
||||||
|
{
|
||||||
|
_recentImportTraceCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DumpRecentImportTrace()
|
||||||
|
{
|
||||||
|
if (_recentImportTraceCount == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] Recent import calls ({_recentImportTraceCount}):");
|
||||||
|
int num = (_recentImportTraceWriteIndex - _recentImportTraceCount + _recentImportTrace.Length) % _recentImportTrace.Length;
|
||||||
|
for (int i = 0; i < _recentImportTraceCount; i++)
|
||||||
|
{
|
||||||
|
int num2 = (num + i) % _recentImportTrace.Length;
|
||||||
|
string text = _recentImportTrace[num2];
|
||||||
|
if (!string.IsNullOrEmpty(text))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] " + text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe static List<ulong> ScanSuspiciousResolverPointers(ulong scanStart, ulong scanEnd)
|
||||||
|
{
|
||||||
|
if (scanEnd <= scanStart)
|
||||||
|
{
|
||||||
|
return new List<ulong>(0);
|
||||||
|
}
|
||||||
|
int num = 0;
|
||||||
|
int num2 = 0;
|
||||||
|
List<ulong> list = new List<ulong>(16);
|
||||||
|
ulong num3 = scanStart;
|
||||||
|
MEMORY_BASIC_INFORMATION64 lpBuffer;
|
||||||
|
while (num3 < scanEnd && VirtualQuery((void*)num3, out lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
|
||||||
|
{
|
||||||
|
ulong baseAddress = lpBuffer.BaseAddress;
|
||||||
|
ulong num4 = baseAddress + lpBuffer.RegionSize;
|
||||||
|
if (num4 <= num3)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ulong value = Math.Max(num3, baseAddress);
|
||||||
|
ulong num5 = Math.Min(num4, scanEnd);
|
||||||
|
if (lpBuffer.State == 4096 && IsReadableProtection(lpBuffer.Protect) && !IsExecutableProtection(lpBuffer.Protect))
|
||||||
|
{
|
||||||
|
ulong num6 = AlignUp(value, 8uL);
|
||||||
|
for (ulong num7 = num6; num7 + 8 <= num5; num7 += 8)
|
||||||
|
{
|
||||||
|
ulong value2 = *(ulong*)num7;
|
||||||
|
if (IsUnresolvedSentinel(value2))
|
||||||
|
{
|
||||||
|
num++;
|
||||||
|
list.Add(num7);
|
||||||
|
if (num2 < 32)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] Suspicious unresolved pointer: slot=0x{num7:X16} value=0x{value2:X16}");
|
||||||
|
num2++;
|
||||||
|
}
|
||||||
|
if (num >= 16384)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][WARNING] Suspicious unresolved pointer scan reached cap ({16384}); truncating.");
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
num3 = num5;
|
||||||
|
}
|
||||||
|
if (num != 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][WARNING] Suspicious unresolved pointer hits: {num}");
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProbeReturnRip(ulong returnRip, long dispatchIndex)
|
||||||
|
{
|
||||||
|
if (_cpuContext == null || returnRip == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Span<byte> destination = stackalloc byte[128];
|
||||||
|
if (!_cpuContext.Memory.TryRead(returnRip, destination))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip probe: unreadable @0x{returnRip:X16}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string value = BitConverter.ToString(destination.ToArray()).Replace("-", " ");
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip bytes @0x{returnRip:X16}: {value}");
|
||||||
|
if (destination[0] == byte.MaxValue && (destination[1] == 21 || destination[1] == 37))
|
||||||
|
{
|
||||||
|
int num = BitConverter.ToInt32(destination.Slice(2, 4));
|
||||||
|
ulong num2 = returnRip + 6 + (ulong)num;
|
||||||
|
if (_cpuContext.TryReadUInt64(num2, out var value2))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip slot: [0x{num2:X16}] = 0x{value2:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (destination[0] == 72 && destination[1] == 139 && destination[2] == 5)
|
||||||
|
{
|
||||||
|
int num3 = BitConverter.ToInt32(destination.Slice(3, 4));
|
||||||
|
ulong num4 = returnRip + 7 + (ulong)num3;
|
||||||
|
if (_cpuContext.TryReadUInt64(num4, out var value3))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip mov-slot: [0x{num4:X16}] = 0x{value3:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i + 6 <= destination.Length; i++)
|
||||||
|
{
|
||||||
|
if (destination[i] == byte.MaxValue && (destination[i + 1] == 21 || destination[i + 1] == 37))
|
||||||
|
{
|
||||||
|
int num5 = BitConverter.ToInt32(destination.Slice(i + 2, 4));
|
||||||
|
ulong num6 = returnRip + (ulong)i;
|
||||||
|
ulong num7 = num6 + 6 + (ulong)num5;
|
||||||
|
if (_cpuContext.TryReadUInt64(num7, out var value4))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} near-indirect @{num6:X16}: slot=0x{num7:X16} val=0x{value4:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsUnresolvedSentinel(ulong value)
|
||||||
|
{
|
||||||
|
return value == 65534 || value == 4294967294u || value == 18446744073709551614uL;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsPlausibleReturnAddress(ulong address)
|
||||||
|
{
|
||||||
|
return address >= 12884901888L && address < 17592186044416L && !IsUnresolvedSentinel(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetPlausibleReturnFromStack(ulong rsp, out ulong returnRip, out ulong nextRsp)
|
||||||
|
{
|
||||||
|
returnRip = 0uL;
|
||||||
|
nextRsp = rsp;
|
||||||
|
if (rsp <= 65536 || rsp >= 140737488355328L)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ulong num = rsp & 0xFFFFFFFFFFFFFFF8uL;
|
||||||
|
ulong num2 = ((num >= 8) ? (num - 8) : num);
|
||||||
|
for (int i = 0; i < 24; i++)
|
||||||
|
{
|
||||||
|
ulong num3 = num2 + (ulong)((long)i * 8L);
|
||||||
|
if (TryReadStackU64(num3, out var value) && IsLikelyReturnAddress(value))
|
||||||
|
{
|
||||||
|
returnRip = value;
|
||||||
|
nextRsp = num3 + 8;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (ulong num4 = 1uL; num4 < 8; num4++)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < 24; j++)
|
||||||
|
{
|
||||||
|
ulong num5 = rsp + num4 + (ulong)((long)j * 8L);
|
||||||
|
if (TryReadStackU64(num5, out var value2) && IsLikelyReturnAddress(value2))
|
||||||
|
{
|
||||||
|
returnRip = value2;
|
||||||
|
ulong num6 = num5 + 8;
|
||||||
|
nextRsp = (num6 + 7) & 0xFFFFFFFFFFFFFFF8uL;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe static bool TryReadStackU64(ulong address, out ulong value)
|
||||||
|
{
|
||||||
|
value = 0uL;
|
||||||
|
if (address <= 65536 || address >= 140737488355328L)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ulong num = lpBuffer.BaseAddress + lpBuffer.RegionSize;
|
||||||
|
if (num < lpBuffer.BaseAddress || address > num - 8)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (lpBuffer.State != 4096 || !IsReadableProtection(lpBuffer.Protect))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
value = *(ulong*)address;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsLikelyReturnAddress(ulong address)
|
||||||
|
{
|
||||||
|
if (!IsPlausibleReturnAddress(address))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return IsExecutableAddress(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe static bool IsExecutableAddress(ulong address)
|
||||||
|
{
|
||||||
|
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong AlignUp(ulong value, ulong alignment)
|
||||||
|
{
|
||||||
|
if (alignment == 0)
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
ulong num = alignment - 1;
|
||||||
|
return (value + num) & ~num;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsReadableProtection(uint protect)
|
||||||
|
{
|
||||||
|
if ((protect & 0x100) != 0 || (protect & 1) != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (protect & 0xEE) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsExecutableProtection(uint protect)
|
||||||
|
{
|
||||||
|
return (protect & 0xF0) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,740 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using SharpEmu.Core.Cpu.Disasm;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
|
public sealed partial class DirectExecutionBackend
|
||||||
|
{
|
||||||
|
private unsafe void SetupExceptionHandler()
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, RawVectoredHandlerPtrManaged);
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] Raw exception handler disabled by SHARPEMU_DISABLE_RAW_HANDLER=1");
|
||||||
|
}
|
||||||
|
|
||||||
|
_handlerDelegate = VectoredHandler;
|
||||||
|
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
|
||||||
|
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
|
||||||
|
|
||||||
|
_unhandledFilterDelegate = UnhandledExceptionFilter;
|
||||||
|
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
|
||||||
|
SetUnhandledExceptionFilter(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
||||||
|
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 248);
|
||||||
|
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 152);
|
||||||
|
Console.Error.WriteLine("[LOADER][FATAL] Unhandled exception filter fired.");
|
||||||
|
Console.Error.WriteLine($"[LOADER][FATAL] Code: 0x{exceptionRecord->ExceptionCode:X8}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][FATAL] Exception Address: 0x{(ulong)(nint)exceptionRecord->ExceptionAddress:X16}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][FATAL] RIP: 0x{rip:X16}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][FATAL] RSP: 0x{rsp:X16}");
|
||||||
|
Console.Error.Flush();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe int VectoredHandler(void* exceptionInfo)
|
||||||
|
{
|
||||||
|
if (_vectoredHandlerDepth > 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[LOADER][TRACE] Nested VEH exception; passing through.");
|
||||||
|
Console.Error.Flush();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_vectoredHandlerDepth++;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
||||||
|
uint exceptionCode = exceptionRecord->ExceptionCode;
|
||||||
|
uint exceptionFlags = exceptionRecord->ExceptionFlags;
|
||||||
|
ulong exceptionAddress = (ulong)exceptionRecord->ExceptionAddress;
|
||||||
|
void* contextRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord;
|
||||||
|
if (contextRecord == null)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[LOADER][FATAL] ContextRecord is null!");
|
||||||
|
Console.Error.Flush();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong rip = ReadCtxU64(contextRecord, 248);
|
||||||
|
ulong rsp = ReadCtxU64(contextRecord, 152);
|
||||||
|
|
||||||
|
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (exceptionCode)
|
||||||
|
{
|
||||||
|
case 3221225477u:
|
||||||
|
LogAccessViolationTrace(exceptionAddress, exceptionRecord);
|
||||||
|
break;
|
||||||
|
case 3221226505u:
|
||||||
|
{
|
||||||
|
ulong p0 = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
|
||||||
|
ulong p1 = exceptionRecord->NumberParameters >= 2 ? exceptionRecord->ExceptionInformation[1] : 0;
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] VEH_FASTFAIL code=0x{exceptionCode:X8} ex=0x{exceptionAddress:X16} rip=0x{rip:X16} rsp=0x{rsp:X16} p0=0x{p0:X16} p1=0x{p1:X16}");
|
||||||
|
Console.Error.Flush();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong rax = ReadCtxU64(contextRecord, 120);
|
||||||
|
ulong rbx = ReadCtxU64(contextRecord, 144);
|
||||||
|
ulong rcx = ReadCtxU64(contextRecord, 128);
|
||||||
|
ulong rdx = ReadCtxU64(contextRecord, 136);
|
||||||
|
ulong rsi = ReadCtxU64(contextRecord, 168);
|
||||||
|
ulong rdi = ReadCtxU64(contextRecord, 176);
|
||||||
|
ulong rbp = ReadCtxU64(contextRecord, 160);
|
||||||
|
ulong r8 = ReadCtxU64(contextRecord, 184);
|
||||||
|
ulong r9 = ReadCtxU64(contextRecord, 192);
|
||||||
|
ulong r10 = ReadCtxU64(contextRecord, 200);
|
||||||
|
ulong r11 = ReadCtxU64(contextRecord, 208);
|
||||||
|
ulong r12 = ReadCtxU64(contextRecord, 216);
|
||||||
|
ulong r13 = ReadCtxU64(contextRecord, 224);
|
||||||
|
ulong r14 = ReadCtxU64(contextRecord, 232);
|
||||||
|
ulong r15 = ReadCtxU64(contextRecord, 240);
|
||||||
|
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] =========================================");
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] NATIVE EXCEPTION CAUGHT!");
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] Code: 0x{exceptionCode:X8}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] Exception Address: 0x{exceptionAddress:X16}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] RIP: 0x{rip:X16}");
|
||||||
|
if (TryFormatNearestRuntimeSymbol(rip, out string symbol))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] RIP symbol: " + symbol);
|
||||||
|
}
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] RAX: 0x{rax:X16}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] RBX: 0x{rbx:X16}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] RCX: 0x{rcx:X16}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] RDX: 0x{rdx:X16}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] RSI: 0x{rsi:X16}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] RDI: 0x{rdi:X16}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] RBP: 0x{rbp:X16}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] RSP: 0x{rsp:X16}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] R8 : 0x{r8:X16}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] R9 : 0x{r9:X16}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] R10: 0x{r10:X16}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] R11: 0x{r11:X16}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] R12: 0x{r12:X16}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] R13: 0x{r13:X16}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] R14: 0x{r14:X16}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] R15: 0x{r15:X16}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] Flags: 0x{exceptionFlags:X8}");
|
||||||
|
|
||||||
|
ulong accessType = 0;
|
||||||
|
ulong target = 0;
|
||||||
|
if (exceptionCode == 3221225477u && exceptionRecord->NumberParameters >= 2)
|
||||||
|
{
|
||||||
|
accessType = *exceptionRecord->ExceptionInformation;
|
||||||
|
target = exceptionRecord->ExceptionInformation[1];
|
||||||
|
string accessText = accessType switch
|
||||||
|
{
|
||||||
|
0uL => "read",
|
||||||
|
1uL => "write",
|
||||||
|
8uL => "execute",
|
||||||
|
_ => $"unknown({accessType})"
|
||||||
|
};
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] AV access: " + accessText);
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] AV target: 0x{target:X16}");
|
||||||
|
if (VirtualQuery((void*)target, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] AV target region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} state=0x{mbi.State:X08} protect=0x{mbi.Protect:X08}");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] Stack qwords (RSP..):");
|
||||||
|
for (int i = 0; i < 16; i++)
|
||||||
|
{
|
||||||
|
ulong stackAddr = rsp + (ulong)(i * 8);
|
||||||
|
ulong value = (ulong)Marshal.ReadInt64((nint)stackAddr);
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] [rsp+0x{i * 8:X2}] @0x{stackAddr:X16} = 0x{value:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[LOADER][WARNING] Could not read stack qwords.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] Frame chain (RBP walk):");
|
||||||
|
ulong frame = rbp;
|
||||||
|
for (int i = 0; i < 12; i++)
|
||||||
|
{
|
||||||
|
if (frame < 140733193388032L || frame > 140737488355327L)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ulong next = (ulong)Marshal.ReadInt64((nint)frame);
|
||||||
|
ulong ret = (ulong)Marshal.ReadInt64((nint)(frame + 8));
|
||||||
|
string extra = TryFormatNearestRuntimeSymbol(ret, out string retSym) ? $" [{retSym}]" : string.Empty;
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] frame#{i}: rbp=0x{frame:X16} ret=0x{ret:X16}{extra} next=0x{next:X16}");
|
||||||
|
if (next <= frame)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
frame = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[LOADER][WARNING] Could not walk RBP frame chain.");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (exceptionCode)
|
||||||
|
{
|
||||||
|
case 3221225477u:
|
||||||
|
Console.Error.WriteLine("[LOADER][ERROR] Type: Access Violation");
|
||||||
|
Console.Error.WriteLine("[LOADER][ERROR] This usually means:");
|
||||||
|
Console.Error.WriteLine("[LOADER][ERROR] - Guest code called an unmapped import");
|
||||||
|
Console.Error.WriteLine("[LOADER][ERROR] - Guest code accessed unmapped memory");
|
||||||
|
Console.Error.WriteLine("[LOADER][ERROR] - Need to implement HLE for this NID");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] code = new byte[16];
|
||||||
|
Marshal.Copy((nint)rip, code, 0, code.Length);
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] Code at RIP: " + BitConverter.ToString(code).Replace("-", " "));
|
||||||
|
if (code[0] == 100)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[LOADER][ERROR] Detected FS segment prefix - TLS access not patched!");
|
||||||
|
}
|
||||||
|
else if (code[0] == 101)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[LOADER][ERROR] Detected GS segment prefix - TLS access not patched!");
|
||||||
|
}
|
||||||
|
else if (code[0] == 197 || code[0] == 196)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] Detected AVX instruction - check CPU support!");
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] RBP: 0x{rbp:X16} (mod 16 = {rbp % 16})");
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] RSP: 0x{rsp:X16} (mod 16 = {rsp % 16})");
|
||||||
|
}
|
||||||
|
if (rip > 16)
|
||||||
|
{
|
||||||
|
byte[] before = new byte[16];
|
||||||
|
Marshal.Copy((nint)(rip - 16), before, 0, before.Length);
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] Code before RIP: " + BitConverter.ToString(before).Replace("-", " "));
|
||||||
|
}
|
||||||
|
if (rip > 32)
|
||||||
|
{
|
||||||
|
byte[] window = new byte[64];
|
||||||
|
Marshal.Copy((nint)(rip - 32), window, 0, window.Length);
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] Code window [RIP-0x20..]: " + BitConverter.ToString(window).Replace("-", " "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[LOADER][ERROR] Could not read code at RIP");
|
||||||
|
}
|
||||||
|
DumpRecentImportTrace();
|
||||||
|
break;
|
||||||
|
case 2147483651u:
|
||||||
|
Console.Error.WriteLine("[LOADER][WARNING] Type: Breakpoint (int3)");
|
||||||
|
Console.Error.WriteLine("[LOADER][WARNING] Unexpected breakpoint in direct-bridge mode");
|
||||||
|
break;
|
||||||
|
case 3221225501u:
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] Type: Illegal Instruction");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] =========================================");
|
||||||
|
Console.Error.Flush();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_vectoredHandlerDepth--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void LogAccessViolationTrace(ulong exceptionAddress, EXCEPTION_RECORD* exceptionRecord)
|
||||||
|
{
|
||||||
|
ulong accessType = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
|
||||||
|
ulong target = exceptionRecord->NumberParameters >= 2 ? exceptionRecord->ExceptionInformation[1] : 0;
|
||||||
|
if (_lastAvTraceRip == exceptionAddress && _lastAvTraceType == accessType && _lastAvTraceTarget == target)
|
||||||
|
{
|
||||||
|
_lastAvTraceRepeatCount++;
|
||||||
|
if (_lastAvTraceRepeatCount > 4 && _lastAvTraceRepeatCount % 128 != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] VEH_AV repeat#{_lastAvTraceRepeatCount} at 0x{exceptionAddress:X16} type={accessType} target=0x{target:X16}");
|
||||||
|
Console.Error.Flush();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastAvTraceRip = exceptionAddress;
|
||||||
|
_lastAvTraceType = accessType;
|
||||||
|
_lastAvTraceTarget = target;
|
||||||
|
_lastAvTraceRepeatCount = 1;
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] VEH_AV first-chance at 0x{exceptionAddress:X16} type={accessType} target=0x{target:X16}");
|
||||||
|
Console.Error.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DumpGuestInstructionStream(string name, ulong startRip, int maxInstructions)
|
||||||
|
{
|
||||||
|
if (_cpuContext == null || startRip < 0x10000 || maxInstructions <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] {name} disasm @0x{startRip:X16}:");
|
||||||
|
ulong rip = startRip;
|
||||||
|
for (int i = 0; i < maxInstructions; i++)
|
||||||
|
{
|
||||||
|
if (!IcedDecoder.TryReadGuestBytes(_cpuContext.Memory, rip, maxLen: 15, out var bytes) ||
|
||||||
|
!IcedDecoder.TryDecode(rip, bytes, out var instruction))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] 0x{rip:X16}: <decode-failed>");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] 0x{instruction.Rip:X16}: {instruction.Text} bytes={IcedDecoder.FormatBytes(instruction.Bytes)}");
|
||||||
|
rip += (ulong)instruction.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DumpUnresolvedSentinelWindow(string name, ulong baseAddress, int size)
|
||||||
|
{
|
||||||
|
if (baseAddress < 0x10000 || size <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong scanStart = baseAddress;
|
||||||
|
ulong scanEnd = baseAddress + (ulong)size;
|
||||||
|
List<ulong> hits = ScanSuspiciousResolverPointers(scanStart, scanEnd);
|
||||||
|
if (hits.Count == 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] {name} unresolved scan: none");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] {name} unresolved scan hits: {hits.Count}");
|
||||||
|
for (int i = 0; i < hits.Count && i < 8; i++)
|
||||||
|
{
|
||||||
|
ulong slotAddress = hits[i];
|
||||||
|
if (TryReadQword(slotAddress, out var value))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] hit#{i}: slot=0x{slotAddress:X16} value=0x{value:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DumpSentinelPatternWindow(string name, ulong baseAddress, int size)
|
||||||
|
{
|
||||||
|
if (_cpuContext == null || baseAddress < 0x10000 || size <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] buffer = new byte[size];
|
||||||
|
if (!_cpuContext.Memory.TryRead(baseAddress, buffer))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] {name} sentinel-pattern scan: unreadable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<string> hits = new();
|
||||||
|
for (int offset = 0; offset + 2 <= buffer.Length; offset++)
|
||||||
|
{
|
||||||
|
if (BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(offset, 2)) == 0xFFFE)
|
||||||
|
{
|
||||||
|
hits.Add($"+0x{offset:X}:u16");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset + 4 <= buffer.Length &&
|
||||||
|
BinaryPrimitives.ReadUInt32LittleEndian(buffer.AsSpan(offset, 4)) == 0xFFFFFFFEu)
|
||||||
|
{
|
||||||
|
hits.Add($"+0x{offset:X}:u32");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset + 8 <= buffer.Length &&
|
||||||
|
BinaryPrimitives.ReadUInt64LittleEndian(buffer.AsSpan(offset, 8)) == 0xFFFFFFFFFFFFFFFEuL)
|
||||||
|
{
|
||||||
|
hits.Add($"+0x{offset:X}:u64");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hits.Count == 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] {name} sentinel-pattern scan: none");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] {name} sentinel-pattern hits: {string.Join(", ", hits.GetRange(0, Math.Min(hits.Count, 12)))}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DumpReturnTargetCandidates(ulong rsp)
|
||||||
|
{
|
||||||
|
if (rsp < 0x10000)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong start = rsp >= 0x10 ? rsp - 0x10 : rsp;
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] Return-target candidates near RSP=0x{rsp:X16}:");
|
||||||
|
for (int offset = 0; offset <= 0x20; offset++)
|
||||||
|
{
|
||||||
|
ulong address = start + (ulong)offset;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ulong value = (ulong)Marshal.ReadInt64((nint)address);
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] [0x{address:X16}] -> 0x{value:X16}");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] [0x{address:X16}] -> <unreadable>");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DumpObjectFieldTargets(string name, ulong objectAddress, int[] offsets, int windowSize)
|
||||||
|
{
|
||||||
|
if (objectAddress < 0x10000 || offsets.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (int offset in offsets)
|
||||||
|
{
|
||||||
|
ulong slotAddress = objectAddress + (ulong)offset;
|
||||||
|
if (!TryReadQword(slotAddress, out var target) || target < 0x10000)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] {name}+0x{offset:X2} target = {FormatPointerWithNearestSymbol(target)}");
|
||||||
|
DumpPointerWindow($"{name}+0x{offset:X2}", target, windowSize);
|
||||||
|
DumpUnresolvedSentinelWindow($"{name}+0x{offset:X2}", target, 0x80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DumpSuspiciousGlobalSlots()
|
||||||
|
{
|
||||||
|
DumpAbsoluteSlot("callback_slot[0x80293BD08]", 0x000000080293BD08uL);
|
||||||
|
DumpAbsoluteSlot("callback_arg[0x8030FBBE8]", 0x00000008030FBBE8uL);
|
||||||
|
DumpAbsoluteSlot("tsc_freq_global[0x8030FD590]", 0x00000008030FD590uL);
|
||||||
|
DumpAbsoluteSlot("tsc_base_global[0x8030FD598]", 0x00000008030FD598uL);
|
||||||
|
DumpAbsoluteSlot("plt_got[0x8028F6100]", 0x00000008028F6100uL);
|
||||||
|
DumpAbsoluteSlot("plt_got[0x8028F6158]", 0x00000008028F6158uL);
|
||||||
|
DumpAbsoluteSlot("plt_got[0x8028F6160]", 0x00000008028F6160uL);
|
||||||
|
DumpAbsoluteSlot("plt_got[0x8028F64C0]", 0x00000008028F64C0uL);
|
||||||
|
DumpAbsoluteSlot("plt_got[0x8028F64C8]", 0x00000008028F64C8uL);
|
||||||
|
DumpAbsoluteSlot("plt_got[0x8028F6590]", 0x00000008028F6590uL);
|
||||||
|
DumpAbsoluteSlot("plt_got[0x8028F6708]", 0x00000008028F6708uL);
|
||||||
|
DumpUnresolvedSentinelWindow("PLT-GOT", 0x00000008028F6100uL, 0x700);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DumpAbsoluteSlot(string name, ulong slotAddress)
|
||||||
|
{
|
||||||
|
if (!TryReadQword(slotAddress, out var value))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] {name} @0x{slotAddress:X16} = <unreadable>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] {name} @0x{slotAddress:X16} = {FormatPointerWithNearestSymbol(value)}");
|
||||||
|
}
|
||||||
|
private void DumpPointerWindow(string name, ulong baseAddress, int size)
|
||||||
|
{
|
||||||
|
if (baseAddress < 0x10000 || size <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] {name} window @0x{baseAddress:X16}:");
|
||||||
|
for (int offset = 0; offset < size; offset += 8)
|
||||||
|
{
|
||||||
|
ulong slotAddress = baseAddress + (ulong)offset;
|
||||||
|
if (!TryReadQword(slotAddress, out var value))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] +0x{offset:X2}: <unreadable>");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] +0x{offset:X2}: {FormatPointerWithNearestSymbol(value)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool TryReadQword(ulong address, out ulong value)
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
if (address < 0x10000)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong regionEnd = mbi.BaseAddress + mbi.RegionSize;
|
||||||
|
if (mbi.State != MEM_COMMIT || !IsReadableProtection(mbi.Protect) || regionEnd <= address || address > regionEnd - 8)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
value = (ulong)Marshal.ReadInt64((nint)address);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatPointerWithNearestSymbol(ulong value)
|
||||||
|
{
|
||||||
|
string text = $"0x{value:X16}";
|
||||||
|
if (TryFormatNearestRuntimeSymbol(value, out string symbol))
|
||||||
|
{
|
||||||
|
text += $" [{symbol}]";
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeRuntimeSymbolIndex(IReadOnlyDictionary<string, ulong> runtimeSymbols)
|
||||||
|
{
|
||||||
|
_runtimeSymbolsByName.Clear();
|
||||||
|
if (runtimeSymbols.Count == 0)
|
||||||
|
{
|
||||||
|
_runtimeSymbolsByAddress = Array.Empty<KeyValuePair<string, ulong>>();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<KeyValuePair<string, ulong>> list = new(runtimeSymbols.Count);
|
||||||
|
foreach (KeyValuePair<string, ulong> runtimeSymbol in runtimeSymbols)
|
||||||
|
{
|
||||||
|
if (runtimeSymbol.Value != 0L && !string.IsNullOrWhiteSpace(runtimeSymbol.Key))
|
||||||
|
{
|
||||||
|
list.Add(runtimeSymbol);
|
||||||
|
_runtimeSymbolsByName[runtimeSymbol.Key] = runtimeSymbol.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Sort((a, b) => a.Value.CompareTo(b.Value));
|
||||||
|
_runtimeSymbolsByAddress = list.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryFormatNearestRuntimeSymbol(ulong address, out string text)
|
||||||
|
{
|
||||||
|
text = string.Empty;
|
||||||
|
KeyValuePair<string, ulong>[] runtimeSymbolsByAddress = _runtimeSymbolsByAddress;
|
||||||
|
if (runtimeSymbolsByAddress.Length == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int low = 0;
|
||||||
|
int high = runtimeSymbolsByAddress.Length - 1;
|
||||||
|
int best = -1;
|
||||||
|
while (low <= high)
|
||||||
|
{
|
||||||
|
int mid = low + ((high - low) >> 1);
|
||||||
|
ulong value = runtimeSymbolsByAddress[mid].Value;
|
||||||
|
if (value <= address)
|
||||||
|
{
|
||||||
|
best = mid;
|
||||||
|
low = mid + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
high = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (best < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyValuePair<string, ulong> symbol = runtimeSymbolsByAddress[best];
|
||||||
|
ulong delta = address - symbol.Value;
|
||||||
|
text = delta == 0
|
||||||
|
? $"{symbol.Key} (0x{symbol.Value:X16})"
|
||||||
|
: $"{symbol.Key}+0x{delta:X} (0x{symbol.Value:X16})";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe static bool TryHandleLazyCommittedPage(EXCEPTION_RECORD* exceptionRecord)
|
||||||
|
{
|
||||||
|
if (exceptionRecord->NumberParameters < 2)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong accessType = *exceptionRecord->ExceptionInformation;
|
||||||
|
ulong faultAddress = exceptionRecord->ExceptionInformation[1];
|
||||||
|
if (accessType == 8 && faultAddress < 4294967296L)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (faultAddress < 65536 || faultAddress >= 140737488355328L)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (VirtualQuery((void*)faultAddress, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
|
||||||
|
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect);
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query: fault=0x{faultAddress:X16} state=0x{mbi.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
|
||||||
|
|
||||||
|
bool committed = false;
|
||||||
|
ulong committedBase = 0;
|
||||||
|
ulong committedSize = 0;
|
||||||
|
|
||||||
|
if (mbi.State == 65536)
|
||||||
|
{
|
||||||
|
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||||
|
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
|
||||||
|
{
|
||||||
|
committed = true;
|
||||||
|
committedBase = largeBase;
|
||||||
|
committedSize = 2097152uL;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
||||||
|
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
|
||||||
|
{
|
||||||
|
committed = true;
|
||||||
|
committedBase = region64kBase;
|
||||||
|
committedSize = 65536uL;
|
||||||
|
}
|
||||||
|
else if (TryReserveThenCommit(pageBase, 4096uL, pageBase, 4096uL, commitProtect))
|
||||||
|
{
|
||||||
|
committed = true;
|
||||||
|
committedBase = pageBase;
|
||||||
|
committedSize = 4096uL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!committed)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mbi.State != 8192)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||||
|
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
|
||||||
|
{
|
||||||
|
committed = true;
|
||||||
|
committedBase = largeCommitBase;
|
||||||
|
committedSize = 2097152uL;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
||||||
|
if (TryCommitRange(region64kBase, 65536uL, commitProtect))
|
||||||
|
{
|
||||||
|
committed = true;
|
||||||
|
committedBase = region64kBase;
|
||||||
|
committedSize = 65536uL;
|
||||||
|
}
|
||||||
|
else if (TryCommitRange(pageBase, 8192uL, commitProtect))
|
||||||
|
{
|
||||||
|
committed = true;
|
||||||
|
committedBase = pageBase;
|
||||||
|
committedSize = 8192uL;
|
||||||
|
}
|
||||||
|
else if (TryCommitRange(pageBase, 4096uL, commitProtect))
|
||||||
|
{
|
||||||
|
committed = true;
|
||||||
|
committedBase = pageBase;
|
||||||
|
committedSize = 4096uL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!committed)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||||
|
return true;
|
||||||
|
|
||||||
|
static unsafe bool TryCommitRange(ulong baseAddress, ulong length, uint protection)
|
||||||
|
{
|
||||||
|
if (length == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return VirtualAlloc((void*)baseAddress, (nuint)length, 4096u, protection) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static unsafe bool TryReserveRange(ulong baseAddress, ulong length)
|
||||||
|
{
|
||||||
|
if (length == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return VirtualAlloc((void*)baseAddress, (nuint)length, 8192u, 4u) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool TryReserveThenCommit(ulong reserveAddress, ulong reserveSize, ulong commitAddress, ulong commitSize, uint protection)
|
||||||
|
{
|
||||||
|
if (!TryReserveRange(reserveAddress, reserveSize))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return TryCommitRange(commitAddress, commitSize, protection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint ResolveLazyCommitProtection(ulong accessType, uint allocationProtect)
|
||||||
|
{
|
||||||
|
if (accessType == 8 || (allocationProtect & 0xF0) != 0)
|
||||||
|
{
|
||||||
|
return 64u;
|
||||||
|
}
|
||||||
|
return 4u;
|
||||||
|
}
|
||||||
|
}
|
||||||
760
src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs
Normal file
760
src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs
Normal file
@@ -0,0 +1,760 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
|
using SharpEmu.Core.Cpu;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
|
public sealed partial class DirectExecutionBackend
|
||||||
|
{
|
||||||
|
private static ulong ImportDispatchGatewayManaged(nint backendHandle, int importIndex, nint argPackPtr)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!(GCHandle.FromIntPtr(backendHandle).Target is DirectExecutionBackend directExecutionBackend))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][ERROR] ImportDispatchGatewayManaged: invalid backend handle 0x{backendHandle:X16}");
|
||||||
|
return 18446744071562199042uL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return directExecutionBackend.DispatchImport(importIndex, argPackPtr);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][ERROR] ImportDispatchGatewayManaged exception: {ex.GetType().Name}: {ex.Message}");
|
||||||
|
return 18446744071562199298uL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe static int RawVectoredHandlerManaged(void* exceptionInfo)
|
||||||
|
{
|
||||||
|
return TryRecoverUnresolvedSentinel(exceptionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe static int RawUnhandledFilterManaged(void* exceptionInfo)
|
||||||
|
{
|
||||||
|
return TryRecoverUnresolvedSentinel(exceptionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe static int TryRecoverUnresolvedSentinel(void* exceptionInfo)
|
||||||
|
{
|
||||||
|
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
||||||
|
if (exceptionRecord->ExceptionCode != 3221225477u)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
void* contextRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord;
|
||||||
|
ulong value = ReadCtxU64(contextRecord, 248);
|
||||||
|
ulong value2 = (ulong)exceptionRecord->ExceptionAddress;
|
||||||
|
if (!IsUnresolvedSentinel(value) && !IsUnresolvedSentinel(value2))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
ulong rsp = ReadCtxU64(contextRecord, 152);
|
||||||
|
WriteCtxU64(contextRecord, 120, 0uL);
|
||||||
|
if (TryGetPlausibleReturnFromStack(rsp, out var returnRip, out var nextRsp))
|
||||||
|
{
|
||||||
|
WriteCtxU64(contextRecord, 152, nextRsp);
|
||||||
|
WriteCtxU64(contextRecord, 248, returnRip);
|
||||||
|
Interlocked.Increment(ref _rawSentinelRecoveries);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe ulong DispatchImport(int importIndex, nint argPackPtr)
|
||||||
|
{
|
||||||
|
long num = Interlocked.Increment(ref _importDispatchCount);
|
||||||
|
MarkExecutionProgress();
|
||||||
|
if (_cpuContext == null)
|
||||||
|
{
|
||||||
|
LastError = "Import dispatch called without active CPU context";
|
||||||
|
return 18446744071562199298uL;
|
||||||
|
}
|
||||||
|
if ((uint)importIndex >= (uint)_importEntries.Length)
|
||||||
|
{
|
||||||
|
LastError = $"Import dispatch index out of range: {importIndex}";
|
||||||
|
return 18446744071562199042uL;
|
||||||
|
}
|
||||||
|
ImportStubEntry importStubEntry = _importEntries[importIndex];
|
||||||
|
int num2 = Volatile.Read(in _rawSentinelRecoveries);
|
||||||
|
if (num2 != _lastReportedRawSentinelRecoveries)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] Raw sentinel recoveries: {num2} (last import index={importIndex})");
|
||||||
|
_lastReportedRawSentinelRecoveries = num2;
|
||||||
|
}
|
||||||
|
_cpuContext.Rip = importStubEntry.Address;
|
||||||
|
_cpuContext[CpuRegister.Rdi] = *(ulong*)argPackPtr;
|
||||||
|
_cpuContext[CpuRegister.Rsi] = *(ulong*)(argPackPtr + 8);
|
||||||
|
_cpuContext[CpuRegister.Rdx] = *(ulong*)(argPackPtr + 16);
|
||||||
|
_cpuContext[CpuRegister.Rcx] = *(ulong*)(argPackPtr + 24);
|
||||||
|
_cpuContext[CpuRegister.R8] = *(ulong*)(argPackPtr + 32);
|
||||||
|
_cpuContext[CpuRegister.R9] = *(ulong*)(argPackPtr + 40);
|
||||||
|
_cpuContext[CpuRegister.Rbx] = *(ulong*)(argPackPtr + 48);
|
||||||
|
_cpuContext[CpuRegister.Rbp] = *(ulong*)(argPackPtr + 56);
|
||||||
|
_cpuContext[CpuRegister.R12] = *(ulong*)(argPackPtr + 64);
|
||||||
|
_cpuContext[CpuRegister.R13] = *(ulong*)(argPackPtr + 72);
|
||||||
|
_cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
|
||||||
|
_cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
|
||||||
|
_cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
|
||||||
|
ulong value = _cpuContext[CpuRegister.Rdi];
|
||||||
|
ulong value2 = _cpuContext[CpuRegister.Rsi];
|
||||||
|
ulong num3 = _cpuContext[CpuRegister.Rdx];
|
||||||
|
ulong num4 = _cpuContext[CpuRegister.Rcx];
|
||||||
|
ulong num5 = _cpuContext[CpuRegister.R8];
|
||||||
|
ulong num6 = _cpuContext[CpuRegister.R9];
|
||||||
|
ulong value3 = _cpuContext[CpuRegister.Rbx];
|
||||||
|
ulong value4 = _cpuContext[CpuRegister.Rbp];
|
||||||
|
ulong value5 = _cpuContext[CpuRegister.R12];
|
||||||
|
ulong value6 = _cpuContext[CpuRegister.R13];
|
||||||
|
ulong value7 = _cpuContext[CpuRegister.R14];
|
||||||
|
ulong value8 = _cpuContext[CpuRegister.R15];
|
||||||
|
ulong num7 = *(ulong*)(argPackPtr + 96);
|
||||||
|
if (!IsLikelyReturnAddress(num7))
|
||||||
|
{
|
||||||
|
for (int i = 1; i <= 4; i++)
|
||||||
|
{
|
||||||
|
ulong num8 = *(ulong*)(argPackPtr + 96 + i * 8);
|
||||||
|
if (IsLikelyReturnAddress(num8))
|
||||||
|
{
|
||||||
|
*(ulong*)(argPackPtr + 96) = num8;
|
||||||
|
num7 = num8;
|
||||||
|
Console.Error.WriteLine($"[LOADER][WARNING] Import#{num}: corrected suspicious return RIP using stack slot +0x{i * 8:X} -> 0x{num7:X16}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TrackDistinctImportNid(importStubEntry.Nid);
|
||||||
|
TrackStrlenPrelude(importStubEntry.Nid, num, num7);
|
||||||
|
bool logBootstrap = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_BOOTSTRAP"), "1", StringComparison.Ordinal);
|
||||||
|
if (logBootstrap && string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
string symbolText = "<unreadable>";
|
||||||
|
if (TryReadAsciiZ(value2, 256, out var sym))
|
||||||
|
{
|
||||||
|
symbolText = sym;
|
||||||
|
}
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] bootstrap_call#{num}: op=0x{value:X16} sym_ptr=0x{value2:X16} sym='{symbolText}' out_ptr=0x{num3:X16} ret=0x{num7:X16}");
|
||||||
|
}
|
||||||
|
if (!_forcedGuestExit && ShouldForceGuestExitOnImportLoop(importStubEntry.Nid, num7, num, value, value2) && TryForceGuestExitToHostStub(argPackPtr, num, num7, importStubEntry.Nid))
|
||||||
|
{
|
||||||
|
_cpuContext[CpuRegister.Rax] = 1uL;
|
||||||
|
return 1uL;
|
||||||
|
}
|
||||||
|
bool flag0 = ShouldSuppressStrlenTrace(importStubEntry.Nid);
|
||||||
|
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
|
||||||
|
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
|
||||||
|
bool flag3 = num >= 1020 && num <= 1040;
|
||||||
|
if (!flag0 && (num <= 128 || (num >= 240 && num <= 400) || (num >= 900 && num <= 1300) || num % 100000 == 0L || (importStubEntry.Nid == "tsvEmnenz48" && (num <= 256 || num % 1000 == 0L)) || (importStubEntry.Nid == "rTXw65xmLIA" && (num <= 256 || num % 128 == 0)) || flag || flag2 || flag3))
|
||||||
|
{
|
||||||
|
if (_moduleManager.TryGetExport(importStubEntry.Nid, out ExportedFunction export))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] Import#{num}: {export.LibraryName}:{export.Name} ({importStubEntry.Nid})");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] Import#{num}: {importStubEntry.Nid}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!flag0)
|
||||||
|
{
|
||||||
|
RecordRecentImportTrace($"#{num} nid={importStubEntry.Nid} ret=0x{num7:X16} rdi=0x{_cpuContext[CpuRegister.Rdi]:X16} rsi=0x{_cpuContext[CpuRegister.Rsi]:X16} rdx=0x{_cpuContext[CpuRegister.Rdx]:X16}");
|
||||||
|
}
|
||||||
|
if (importStubEntry.Nid == "8zTFvBIAIN8" && num <= 256)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] memset#{num}: dst=0x{_cpuContext[CpuRegister.Rdi]:X16} val=0x{_cpuContext[CpuRegister.Rsi] & 0xFF:X2} len=0x{_cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
|
||||||
|
}
|
||||||
|
if (importStubEntry.Nid == "tsvEmnenz48" && num <= 64)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] __cxa_atexit#{num}: func=0x{_cpuContext[CpuRegister.Rdi]:X16} arg=0x{_cpuContext[CpuRegister.Rsi]:X16} dso=0x{_cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
|
||||||
|
}
|
||||||
|
if (importStubEntry.Nid == "bzQExy189ZI" || importStubEntry.Nid == "8G2LB+A3rzg")
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] {importStubEntry.Nid}#{num}: rdi=0x{_cpuContext[CpuRegister.Rdi]:X16} rsi=0x{_cpuContext[CpuRegister.Rsi]:X16} rdx=0x{_cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
|
||||||
|
}
|
||||||
|
if (flag || flag2 || flag3)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] ImportCtx#{num}: nid={importStubEntry.Nid} ret=0x{num7:X16} rdi=0x{_cpuContext[CpuRegister.Rdi]:X16} rsi=0x{_cpuContext[CpuRegister.Rsi]:X16} rdx=0x{_cpuContext[CpuRegister.Rdx]:X16} rcx=0x{_cpuContext[CpuRegister.Rcx]:X16}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] ImportNV#{num}: rbx=0x{value3:X16} rbp=0x{value4:X16} r12=0x{value5:X16} r13=0x{value6:X16} r14=0x{value7:X16} r15=0x{value8:X16}");
|
||||||
|
if (flag3)
|
||||||
|
{
|
||||||
|
ulong num9 = _cpuContext[CpuRegister.Rsp];
|
||||||
|
if (_cpuContext.TryReadUInt64(num9, out var value9) && _cpuContext.TryReadUInt64(num9 + 8, out var value10) && _cpuContext.TryReadUInt64(num9 + 16, out var value11) && _cpuContext.TryReadUInt64(num9 + 24, out var value12) && _cpuContext.TryReadUInt64(num9 + 32, out var value13) && _cpuContext.TryReadUInt64(num9 + 40, out var value14) && _cpuContext.TryReadUInt64(num9 + 48, out var value15) && _cpuContext.TryReadUInt64(num9 + 56, out var value16) && _cpuContext.TryReadUInt64(num9 + 64, out var value17))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] ImportStackHead#{num}: rsp=0x{num9:X16} [0]=0x{value9:X16} [20]=0x{value13:X16} [40]=0x{value17:X16}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] ImportStack#{num}: rsp=0x{num9:X16} [0]=0x{value9:X16} [8]=0x{value10:X16} [10]=0x{value11:X16} [18]=0x{value12:X16} [20]=0x{value13:X16} [28]=0x{value14:X16} [30]=0x{value15:X16} [38]=0x{value16:X16} [40]=0x{value17:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (flag3)
|
||||||
|
{
|
||||||
|
Console.Error.Flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (importStubEntry.Nid == "Ou3iL1abvng")
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] array = new byte[64];
|
||||||
|
Marshal.Copy((nint)(num7 - 32), array, 0, array.Length);
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] __stack_chk_fail return-site @0x{num7:X16}: {BitConverter.ToString(array).Replace("-", " ")}");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
TryBypassStackChkFailTrap(num, num7);
|
||||||
|
}
|
||||||
|
if (importStubEntry.Nid == "9rAeANT2tyE" || importStubEntry.Nid == "1j3S3n-tTW4")
|
||||||
|
{
|
||||||
|
ulong rspDbg = _cpuContext[CpuRegister.Rsp];
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] ImportDbg#{num}: nid={importStubEntry.Nid} " +
|
||||||
|
$"ret=0x{num7:X16} rsp=0x{rspDbg:X16} rsp&7=0x{(rspDbg & 7):X} rsp&F=0x{(rspDbg & 0xF):X}");
|
||||||
|
|
||||||
|
if (_cpuContext.TryReadUInt64(rspDbg, out var s0))
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] [rsp+0x00] = 0x{s0:X16}");
|
||||||
|
if (_cpuContext.TryReadUInt64(rspDbg + 8, out var s8))
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] [rsp+0x08] = 0x{s8:X16}");
|
||||||
|
if (_cpuContext.TryReadUInt64(rspDbg + 16, out var s10))
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] [rsp+0x10] = 0x{s10:X16}");
|
||||||
|
if (_cpuContext.TryReadUInt64(rspDbg + 24, out var s18))
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] [rsp+0x18] = 0x{s18:X16}");
|
||||||
|
|
||||||
|
ProbeReturnRip(num7, num);
|
||||||
|
Console.Error.Flush();
|
||||||
|
}
|
||||||
|
ProbeReturnRip(0x0000000800EA020Eul, 999001);
|
||||||
|
ProbeReturnRip(0x0000000800EA0213ul, 999002);
|
||||||
|
ProbeReturnRip(0x0000000800EA040Aul, 999003);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
OrbisGen2Result orbisGen2Result;
|
||||||
|
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
orbisGen2Result = DispatchBootstrapBridge();
|
||||||
|
}
|
||||||
|
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
orbisGen2Result = DispatchKernelDynlibDlsym();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
orbisGen2Result = _moduleManager.Dispatch(importStubEntry.Nid, _cpuContext);
|
||||||
|
}
|
||||||
|
switch (orbisGen2Result)
|
||||||
|
{
|
||||||
|
case OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND:
|
||||||
|
LastError = "Missing HLE export for NID: " + importStubEntry.Nid;
|
||||||
|
Console.Error.WriteLine($"[LOADER][WARN] Import#{num} unresolved: nid={importStubEntry.Nid} ret=0x{num7:X16}");
|
||||||
|
if (importStubEntry.Nid == "L-Q3LEjIbgA")
|
||||||
|
{
|
||||||
|
string value18 = string.Join(" ", importStubEntry.Nid.Select(delegate (char c)
|
||||||
|
{
|
||||||
|
int num10 = c;
|
||||||
|
return num10.ToString("X2");
|
||||||
|
}));
|
||||||
|
Console.Error.WriteLine($"[LOADER][WARN] map_direct nid raw len={importStubEntry.Nid.Length} chars=[{value18}]");
|
||||||
|
Delegate function;
|
||||||
|
bool value19 = _moduleManager.TryGetFunction(importStubEntry.Nid, out function);
|
||||||
|
ExportedFunction export2;
|
||||||
|
bool value20 = _moduleManager.TryGetExport(importStubEntry.Nid, out export2);
|
||||||
|
Console.Error.WriteLine($"[LOADER][WARN] map_direct lookup with import nid: function={value19}, export={value20}");
|
||||||
|
Console.Error.WriteLine(_moduleManager.TryGetExport("L-Q3LEjIbgA", out ExportedFunction export3) ? $"[LOADER][WARN] Canonical map_direct exists as {export3.LibraryName}:{export3.Name}, target={export3.Target}, ctx_target={_cpuContext.TargetGeneration}" : "[LOADER][WARN] Canonical map_direct export lookup also missing");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Console.Error.WriteLine($"[LOADER][WARN] Import#{num} result: {orbisGen2Result} ({importStubEntry.Nid})");
|
||||||
|
break;
|
||||||
|
case OrbisGen2Result.ORBIS_GEN2_OK:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_cpuContext[CpuRegister.Rbx] = value3;
|
||||||
|
_cpuContext[CpuRegister.Rbp] = value4;
|
||||||
|
_cpuContext[CpuRegister.R12] = value5;
|
||||||
|
_cpuContext[CpuRegister.R13] = value6;
|
||||||
|
_cpuContext[CpuRegister.R14] = value7;
|
||||||
|
_cpuContext[CpuRegister.R15] = value8;
|
||||||
|
_cpuContext[CpuRegister.Rdi] = value;
|
||||||
|
_cpuContext[CpuRegister.Rsi] = value2;
|
||||||
|
if (flag || flag2 || flag3)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] ImportRet#{num}: nid={importStubEntry.Nid} result={orbisGen2Result} rax=0x{_cpuContext[CpuRegister.Rax]:X16}");
|
||||||
|
if (flag3)
|
||||||
|
{
|
||||||
|
Console.Error.Flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _cpuContext[CpuRegister.Rax];
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LastError = $"HLE dispatch error for {importStubEntry.Nid}: {ex.GetType().Name}: {ex.Message}";
|
||||||
|
_cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
|
||||||
|
return 18446744071562199298uL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool TryForceGuestExitToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid)
|
||||||
|
{
|
||||||
|
ulong num = _entryReturnSentinelRip;
|
||||||
|
if (num < 65536)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
*(ulong*)(argPackPtr + 96) = num;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_forcedGuestExit = true;
|
||||||
|
LastError = $"Detected repeating import loop at import#{dispatchIndex} ({nid}) and forced guest exit.";
|
||||||
|
Console.Error.WriteLine($"[LOADER][ERROR] Import-loop guard fired at import#{dispatchIndex}: nid={nid} ret=0x{returnRip:X16} -> host_exit=0x{num:X16}");
|
||||||
|
DumpRecentImportTrace();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ShouldForceGuestExitOnImportLoop(string nid, ulong returnRip, long dispatchIndex, ulong arg0, ulong arg1)
|
||||||
|
{
|
||||||
|
if (dispatchIndex < 1200)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RecordImportLoopSignature(BuildImportLoopSignature(nid, returnRip, arg0, arg1));
|
||||||
|
if (!HasRepeatingImportLoopPattern())
|
||||||
|
{
|
||||||
|
if (_importLoopPatternHits > 0)
|
||||||
|
{
|
||||||
|
_importLoopPatternHits--;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_importLoopPatternHits++;
|
||||||
|
return _importLoopPatternHits >= 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ulong BuildImportLoopSignature(string nid, ulong returnRip, ulong arg0, ulong arg1)
|
||||||
|
{
|
||||||
|
if (!_importNidHashCache.TryGetValue(nid, out var value))
|
||||||
|
{
|
||||||
|
value = StableHash64(nid);
|
||||||
|
_importNidHashCache[nid] = value;
|
||||||
|
}
|
||||||
|
ulong num = returnRip >> 2;
|
||||||
|
ulong num2 = ((arg0 >> 4) * 11400714819323198485uL) ^ ((arg1 >> 4) * 14029467366897019727uL);
|
||||||
|
return num ^ value * 11400714819323198485uL ^ num2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecordImportLoopSignature(ulong signature)
|
||||||
|
{
|
||||||
|
_importLoopSignatures[_importLoopSignatureWriteIndex] = signature;
|
||||||
|
_importLoopSignatureWriteIndex = (_importLoopSignatureWriteIndex + 1) % _importLoopSignatures.Length;
|
||||||
|
if (_importLoopSignatureCount < _importLoopSignatures.Length)
|
||||||
|
{
|
||||||
|
_importLoopSignatureCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HasRepeatingImportLoopPattern()
|
||||||
|
{
|
||||||
|
int num = _importLoopSignatureCount;
|
||||||
|
if (num < 96)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int num2 = Math.Min(48, num / 4);
|
||||||
|
for (int i = 6; i <= num2; i++)
|
||||||
|
{
|
||||||
|
if (HasRepeatingImportLoopPattern(i, 4))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HasRepeatingImportLoopPattern(int period, int repeats)
|
||||||
|
{
|
||||||
|
int num = period * repeats;
|
||||||
|
if (period <= 0 || repeats < 2 || _importLoopSignatureCount < num)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < period; i++)
|
||||||
|
{
|
||||||
|
ulong importLoopSignatureFromTail = GetImportLoopSignatureFromTail(i);
|
||||||
|
for (int j = 1; j < repeats; j++)
|
||||||
|
{
|
||||||
|
if (GetImportLoopSignatureFromTail(i + j * period) != importLoopSignatureFromTail)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ulong GetImportLoopSignatureFromTail(int offset)
|
||||||
|
{
|
||||||
|
int num = _importLoopSignatureWriteIndex - 1 - offset;
|
||||||
|
while (num < 0)
|
||||||
|
{
|
||||||
|
num += _importLoopSignatures.Length;
|
||||||
|
}
|
||||||
|
return _importLoopSignatures[num % _importLoopSignatures.Length];
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ShouldSuppressStrlenTrace(string nid)
|
||||||
|
{
|
||||||
|
return string.Equals(nid, "j4ViWNHEgww", StringComparison.Ordinal) && !_logStrlenImports;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TrackDistinctImportNid(string nid)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(nid) || string.Equals(_lastDistinctImportNid, nid, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_lastDistinctImportNid = nid;
|
||||||
|
_distinctImportNidHistory[_distinctImportNidHistoryWriteIndex] = nid;
|
||||||
|
_distinctImportNidHistoryWriteIndex = (_distinctImportNidHistoryWriteIndex + 1) % _distinctImportNidHistory.Length;
|
||||||
|
if (_distinctImportNidHistoryCount < _distinctImportNidHistory.Length)
|
||||||
|
{
|
||||||
|
_distinctImportNidHistoryCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TrackStrlenPrelude(string nid, long dispatchIndex, ulong returnRip)
|
||||||
|
{
|
||||||
|
if (!string.Equals(nid, "j4ViWNHEgww", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_consecutiveStrlenImports = 0;
|
||||||
|
_strlenPreludeLogged = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_consecutiveStrlenImports++;
|
||||||
|
if (_strlenPreludeLogged || _consecutiveStrlenImports < 24)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_strlenPreludeLogged = true;
|
||||||
|
List<string> list = GetRecentDistinctImportPrelude(maxCount: 5, skipNid: "j4ViWNHEgww");
|
||||||
|
if (list.Count == 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: detected strlen burst (count={_consecutiveStrlenImports}) ret=0x{returnRip:X16}; no prelude NIDs recorded.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: detected strlen burst (count={_consecutiveStrlenImports}) ret=0x{returnRip:X16}; last5_nids={string.Join(" -> ", list)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> GetRecentDistinctImportPrelude(int maxCount, string skipNid)
|
||||||
|
{
|
||||||
|
List<string> list = new List<string>(maxCount);
|
||||||
|
if (maxCount <= 0 || _distinctImportNidHistoryCount == 0)
|
||||||
|
{
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
for (int i = 0; i < _distinctImportNidHistoryCount && list.Count < maxCount; i++)
|
||||||
|
{
|
||||||
|
int num = _distinctImportNidHistoryWriteIndex - 1 - i;
|
||||||
|
while (num < 0)
|
||||||
|
{
|
||||||
|
num += _distinctImportNidHistory.Length;
|
||||||
|
}
|
||||||
|
string text = _distinctImportNidHistory[num % _distinctImportNidHistory.Length];
|
||||||
|
if (string.IsNullOrWhiteSpace(text) || string.Equals(text, skipNid, StringComparison.Ordinal) || !hashSet.Add(text))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (_moduleManager.TryGetExport(text, out ExportedFunction export))
|
||||||
|
{
|
||||||
|
list.Add($"{export.LibraryName}:{export.Name}({text})");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
list.Add(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.Reverse();
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong StableHash64(string text)
|
||||||
|
{
|
||||||
|
ulong num = 14695981039346656037uL;
|
||||||
|
for (int i = 0; i < text.Length; i++)
|
||||||
|
{
|
||||||
|
num ^= text[i];
|
||||||
|
num *= 1099511628211uL;
|
||||||
|
}
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OrbisGen2Result DispatchKernelDynlibDlsym()
|
||||||
|
{
|
||||||
|
if (_cpuContext == null)
|
||||||
|
{
|
||||||
|
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
ulong symbolNameAddress = _cpuContext[CpuRegister.Rsi];
|
||||||
|
ulong outputAddress = _cpuContext[CpuRegister.Rdx];
|
||||||
|
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName))
|
||||||
|
{
|
||||||
|
_cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
||||||
|
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
if (!TryResolveRuntimeSymbolAddress(symbolName, out var resolvedAddress))
|
||||||
|
{
|
||||||
|
_cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
||||||
|
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
if (outputAddress == 0L || !TryWriteUInt64Compat(outputAddress, resolvedAddress))
|
||||||
|
{
|
||||||
|
_cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
||||||
|
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
_cpuContext[CpuRegister.Rax] = 0uL;
|
||||||
|
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OrbisGen2Result DispatchBootstrapBridge()
|
||||||
|
{
|
||||||
|
if (_cpuContext == null)
|
||||||
|
{
|
||||||
|
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong bridgeHandle = _cpuContext[CpuRegister.Rdi];
|
||||||
|
ulong symbolNameAddress = _cpuContext[CpuRegister.Rsi];
|
||||||
|
ulong outputAddress = _cpuContext[CpuRegister.Rdx];
|
||||||
|
_ = TryReadAsciiZ(symbolNameAddress, 512, out var symbolName);
|
||||||
|
|
||||||
|
OrbisGen2Result result = DispatchKernelDynlibDlsym();
|
||||||
|
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
bool logBootstrap = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_BOOTSTRAP"), "1", StringComparison.Ordinal);
|
||||||
|
if (logBootstrap)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] bootstrap_dispatch: handle=0x{bridgeHandle:X16} symbol='{symbolName}' out=0x{outputAddress:X16} rax=0x{_cpuContext[CpuRegister.Rax]:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_cpuContext[CpuRegister.Rax] == 0uL)
|
||||||
|
{
|
||||||
|
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] bootstrap_bridge unresolved: handle=0x{bridgeHandle:X} symbol='{symbolName}' out=0x{outputAddress:X16}");
|
||||||
|
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryResolveRuntimeSymbolAddress(string symbolName, out ulong address)
|
||||||
|
{
|
||||||
|
address = 0uL;
|
||||||
|
if (string.IsNullOrWhiteSpace(symbolName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (_runtimeSymbolsByName.TryGetValue(symbolName, out var value) && IsRuntimeSymbolAddressUsable(value))
|
||||||
|
{
|
||||||
|
address = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (symbolName.StartsWith("_", StringComparison.Ordinal) && _runtimeSymbolsByName.TryGetValue(symbolName[1..], out value) && IsRuntimeSymbolAddressUsable(value))
|
||||||
|
{
|
||||||
|
address = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (_runtimeSymbolsByName.TryGetValue("_" + symbolName, out value) && IsRuntimeSymbolAddressUsable(value))
|
||||||
|
{
|
||||||
|
address = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsRuntimeSymbolAddressUsable(ulong value)
|
||||||
|
{
|
||||||
|
return value != 0 && !IsUnresolvedSentinel(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryReadAsciiZ(ulong address, int maxLength, out string value)
|
||||||
|
{
|
||||||
|
value = string.Empty;
|
||||||
|
if (_cpuContext == null || address == 0L || maxLength <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
List<byte> list = new List<byte>(Math.Min(maxLength, 256));
|
||||||
|
Span<byte> destination = stackalloc byte[1];
|
||||||
|
for (int i = 0; i < maxLength; i++)
|
||||||
|
{
|
||||||
|
if (!TryReadByteCompat(address + (ulong)i, destination))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (destination[0] == 0)
|
||||||
|
{
|
||||||
|
value = System.Text.Encoding.ASCII.GetString(list.ToArray());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
list.Add(destination[0]);
|
||||||
|
}
|
||||||
|
value = System.Text.Encoding.ASCII.GetString(list.ToArray());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryReadByteCompat(ulong address, Span<byte> destination)
|
||||||
|
{
|
||||||
|
if (_cpuContext == null || destination.Length == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (_cpuContext.Memory.TryRead(address, destination))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
destination[0] = Marshal.ReadByte((nint)address);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryWriteUInt64Compat(ulong address, ulong value)
|
||||||
|
{
|
||||||
|
if (_cpuContext == null || address == 0L)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (_cpuContext.TryWriteUInt64(address, value))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Marshal.WriteInt64((nint)address, unchecked((long)value));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryBypassStackChkFailTrap(long dispatchIndex, ulong returnRip)
|
||||||
|
{
|
||||||
|
if (_cpuContext == null || returnRip < 32)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] array = new byte[19];
|
||||||
|
ulong num = returnRip - 23;
|
||||||
|
Marshal.Copy((nint)num, array, 0, array.Length);
|
||||||
|
if (array[0] != 117 || array[1] != 16 || array[2] != 72 || array[3] != 137 || array[4] != 216 || array[5] != 72 || array[6] != 131 || array[7] != 196 || array[9] != 91 || array[10] != 65 || array[11] != 92 || array[12] != 65 || array[13] != 94 || array[14] != 65 || array[15] != 95 || array[16] != 93 || array[17] != 195 || array[18] != 232)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ulong value = returnRip - 21;
|
||||||
|
ulong address = _cpuContext[CpuRegister.Rsp];
|
||||||
|
if (_cpuContext.TryWriteUInt64(address, value))
|
||||||
|
{
|
||||||
|
if (_stackChkBypassSites.Add(num) && TryPatchStackChkFailBranch(num))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: patched stack_chk_fail tail branch at 0x{num:X16} -> NOP NOP");
|
||||||
|
}
|
||||||
|
Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: redirected __stack_chk_fail return to epilogue 0x{value:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe static bool TryPatchStackChkFailBranch(ulong branchAddress)
|
||||||
|
{
|
||||||
|
uint flNewProtect = default(uint);
|
||||||
|
if (!VirtualProtect((void*)branchAddress, 2u, 64u, &flNewProtect))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Marshal.ReadByte((nint)branchAddress) != 117)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Marshal.WriteByte((nint)branchAddress, 144);
|
||||||
|
Marshal.WriteByte((nint)(branchAddress + 1), 144);
|
||||||
|
FlushInstructionCache(GetCurrentProcess(), (void*)branchAddress, 2u);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
VirtualProtect((void*)branchAddress, 2u, flNewProtect, &flNewProtect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void TryPatchEa020eLookupCall(long dispatchIndex, ulong returnRip)
|
||||||
|
{
|
||||||
|
if (_patchedEa020eLookupCall || returnRip != 0x0000000800EA01A6uL)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ulong num = 0x0000000800EA020EuL;
|
||||||
|
nint num2 = unchecked((nint)num);
|
||||||
|
uint flNewProtect = default(uint);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Marshal.ReadByte(num2) != 232 || !VirtualProtect((void*)num, 5u, 64u, &flNewProtect))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
{
|
||||||
|
Marshal.WriteByte(num2 + i, 144);
|
||||||
|
}
|
||||||
|
FlushInstructionCache(GetCurrentProcess(), (void*)num, 5u);
|
||||||
|
_patchedEa020eLookupCall = true;
|
||||||
|
Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: patched hash-lookup call at 0x{num:X16} -> NOP*5");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (flNewProtect != 0)
|
||||||
|
{
|
||||||
|
VirtualProtect((void*)num, 5u, flNewProtect, &flNewProtect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1555
src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs
Normal file
1555
src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs
Normal file
File diff suppressed because it is too large
Load Diff
22
src/SharpEmu.Core/Cpu/Native/INativeCpuBackend.cs
Normal file
22
src/SharpEmu.Core/Cpu/Native/INativeCpuBackend.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
|
public interface INativeCpuBackend
|
||||||
|
{
|
||||||
|
string BackendName { get; }
|
||||||
|
|
||||||
|
string? LastError { get; }
|
||||||
|
|
||||||
|
bool TryExecute(
|
||||||
|
CpuContext context,
|
||||||
|
ulong entryPoint,
|
||||||
|
Generation generation,
|
||||||
|
IReadOnlyDictionary<ulong, string> importStubs,
|
||||||
|
IReadOnlyDictionary<string, ulong> runtimeSymbols,
|
||||||
|
CpuExecutionOptions executionOptions,
|
||||||
|
out OrbisGen2Result result);
|
||||||
|
}
|
||||||
275
src/SharpEmu.Core/Cpu/Native/JitStubs.cs
Normal file
275
src/SharpEmu.Core/Cpu/Native/JitStubs.cs
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
|
public static unsafe class JitStubs
|
||||||
|
{
|
||||||
|
public const int XsaveBufferSize = 2688;
|
||||||
|
|
||||||
|
public const ulong XsaveChkGuard = 0xDeadBeef5533CCAAu;
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||||
|
public struct JmpWithIndex
|
||||||
|
{
|
||||||
|
private fixed byte _code[16];
|
||||||
|
|
||||||
|
public static ReadOnlySpan<byte> Template => new byte[]
|
||||||
|
{
|
||||||
|
0x68, 0x00, 0x00, 0x00, 0x00, // push <index>
|
||||||
|
0xE9, 0x00, 0x00, 0x00, 0x00, // jmp <handler>
|
||||||
|
0x90, 0x90, 0x90, 0x90, 0x90, 0x90 // nop padding
|
||||||
|
};
|
||||||
|
|
||||||
|
public static int Size => 16;
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
fixed (byte* code = _code)
|
||||||
|
{
|
||||||
|
Template.CopyTo(new Span<byte>(code, 16));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetIndex(uint index)
|
||||||
|
{
|
||||||
|
fixed (byte* code = _code)
|
||||||
|
{
|
||||||
|
*(uint*)(code + 1) = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetHandler(void* handler)
|
||||||
|
{
|
||||||
|
fixed (byte* code = _code)
|
||||||
|
{
|
||||||
|
var funcAddr = (long)handler;
|
||||||
|
var ripAddr = (long)(code + 10); // After push + jmp
|
||||||
|
var offset64 = funcAddr - ripAddr;
|
||||||
|
var offset32 = (uint)(ulong)offset64;
|
||||||
|
|
||||||
|
*(uint*)(code + 6) = offset32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte* GetCodePointer()
|
||||||
|
{
|
||||||
|
fixed (byte* code = _code)
|
||||||
|
{
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||||
|
public struct Call9
|
||||||
|
{
|
||||||
|
private fixed byte _code[9];
|
||||||
|
|
||||||
|
public static ReadOnlySpan<byte> Template => new byte[]
|
||||||
|
{
|
||||||
|
0xE8, 0x00, 0x00, 0x00, 0x00, // call func
|
||||||
|
0x48, 0x89, 0xC0, // mov rax,rax
|
||||||
|
0x90 // nop
|
||||||
|
};
|
||||||
|
|
||||||
|
public static int Size => 9;
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
fixed (byte* code = _code)
|
||||||
|
{
|
||||||
|
Template.CopyTo(new Span<byte>(code, 9));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetFunc(void* func)
|
||||||
|
{
|
||||||
|
fixed (byte* code = _code)
|
||||||
|
{
|
||||||
|
var funcAddr = (long)func;
|
||||||
|
var ripAddr = (long)(code + 5); // After call instruction
|
||||||
|
var offset64 = funcAddr - ripAddr;
|
||||||
|
var offset32 = (uint)(ulong)offset64;
|
||||||
|
|
||||||
|
*(uint*)(code + 1) = offset32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte* GetCodePointer()
|
||||||
|
{
|
||||||
|
fixed (byte* code = _code)
|
||||||
|
{
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||||
|
public struct JmpRax
|
||||||
|
{
|
||||||
|
private fixed byte _code[12];
|
||||||
|
|
||||||
|
public static ReadOnlySpan<byte> Template => new byte[]
|
||||||
|
{
|
||||||
|
0x48, 0xB8, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, // movabs rax,<addr>
|
||||||
|
0xFF, 0xE0 // jmp rax
|
||||||
|
};
|
||||||
|
|
||||||
|
public static int Size => 12;
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
fixed (byte* code = _code)
|
||||||
|
{
|
||||||
|
Template.CopyTo(new Span<byte>(code, 12));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetTarget(void* target)
|
||||||
|
{
|
||||||
|
fixed (byte* code = _code)
|
||||||
|
{
|
||||||
|
*(ulong*)(code + 2) = (ulong)target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte* GetCodePointer()
|
||||||
|
{
|
||||||
|
fixed (byte* code = _code)
|
||||||
|
{
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SafeCall
|
||||||
|
{
|
||||||
|
public static ReadOnlySpan<byte> Template => new byte[]
|
||||||
|
{
|
||||||
|
0x51,
|
||||||
|
0x52,
|
||||||
|
0x41, 0x50,
|
||||||
|
0x41, 0x51,
|
||||||
|
0x41, 0x52,
|
||||||
|
0x41, 0x53,
|
||||||
|
0x57,
|
||||||
|
0x56,
|
||||||
|
0x48, 0xBF, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
|
||||||
|
0x48, 0xBE, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
|
||||||
|
0x48, 0xB9, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
|
||||||
|
0xB0, 0x01,
|
||||||
|
0x86, 0x07,
|
||||||
|
0x84, 0xC0,
|
||||||
|
0x75, 0xF8,
|
||||||
|
0xB8, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xBA, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0x0F, 0xAE, 0x26,
|
||||||
|
0x48, 0x83, 0xEC, 0x08,
|
||||||
|
0xFF, 0xD1,
|
||||||
|
0x48, 0x83, 0xC4, 0x08,
|
||||||
|
0x48, 0x89, 0xC1,
|
||||||
|
0xB8, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xBA, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0x0F, 0xAE, 0x2E,
|
||||||
|
0x48, 0x89, 0xC8,
|
||||||
|
0xC6, 0x07, 0x00,
|
||||||
|
0x5E,
|
||||||
|
0x5F,
|
||||||
|
0x41, 0x5B,
|
||||||
|
0x41, 0x5A,
|
||||||
|
0x41, 0x59,
|
||||||
|
0x41, 0x58,
|
||||||
|
0x5A,
|
||||||
|
0x59,
|
||||||
|
0xC3
|
||||||
|
};
|
||||||
|
|
||||||
|
public static int Size => Template.Length; // 0x6c = 108 bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ReadOnlySpan<byte> TlsAccessPattern => new byte[]
|
||||||
|
{
|
||||||
|
0x64, 0x48, 0x8B, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00
|
||||||
|
};
|
||||||
|
|
||||||
|
public static void CreateJmpWithIndex(byte* location, uint index, void* handler)
|
||||||
|
{
|
||||||
|
|
||||||
|
var code = location;
|
||||||
|
|
||||||
|
code[0] = 0x68;
|
||||||
|
*(uint*)(code + 1) = index;
|
||||||
|
|
||||||
|
code[5] = 0xE9;
|
||||||
|
var handlerAddr = (long)handler;
|
||||||
|
var ripAddr = (long)(code + 10); // After push + jmp
|
||||||
|
var offset64 = handlerAddr - ripAddr;
|
||||||
|
*(uint*)(code + 6) = (uint)(ulong)offset64;
|
||||||
|
|
||||||
|
for (int i = 10; i < 16; i++)
|
||||||
|
{
|
||||||
|
code[i] = 0x90;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CreateCall9(byte* location, void* func)
|
||||||
|
{
|
||||||
|
|
||||||
|
var code = location;
|
||||||
|
|
||||||
|
code[0] = 0xE8;
|
||||||
|
var funcAddr = (long)func;
|
||||||
|
var ripAddr = (long)(code + 5);
|
||||||
|
var offset64 = funcAddr - ripAddr;
|
||||||
|
*(uint*)(code + 1) = (uint)(ulong)offset64;
|
||||||
|
|
||||||
|
code[5] = 0x48;
|
||||||
|
code[6] = 0x89;
|
||||||
|
code[7] = 0xC0;
|
||||||
|
|
||||||
|
code[8] = 0x90;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CreateJmpRax(byte* location, void* target)
|
||||||
|
{
|
||||||
|
|
||||||
|
var code = location;
|
||||||
|
|
||||||
|
code[0] = 0x48;
|
||||||
|
code[1] = 0xB8;
|
||||||
|
*(ulong*)(code + 2) = (ulong)target;
|
||||||
|
|
||||||
|
code[10] = 0xFF;
|
||||||
|
code[11] = 0xE0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<nint> FindTlsAccessPatterns(byte* start, int length)
|
||||||
|
{
|
||||||
|
var results = new List<nint>();
|
||||||
|
var pattern = TlsAccessPattern;
|
||||||
|
var end = start + length - pattern.Length;
|
||||||
|
|
||||||
|
for (var ptr = start; ptr < end; ptr++)
|
||||||
|
{
|
||||||
|
if (MatchesPattern(ptr, pattern))
|
||||||
|
{
|
||||||
|
results.Add((nint)ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchesPattern(byte* ptr, ReadOnlySpan<byte> pattern)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < pattern.Length; i++)
|
||||||
|
{
|
||||||
|
if (ptr[i] != pattern[i])
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
222
src/SharpEmu.Core/Cpu/Native/StubManager.cs
Normal file
222
src/SharpEmu.Core/Cpu/Native/StubManager.cs
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
|
public sealed unsafe class StubManager : IDisposable
|
||||||
|
{
|
||||||
|
private readonly List<nint> _allocatedStubs = new();
|
||||||
|
private readonly Dictionary<string, nint> _importHandlers = new();
|
||||||
|
private readonly Dictionary<ulong, nint> _stubAddresses = new();
|
||||||
|
private byte* _pltMemory;
|
||||||
|
private int _pltOffset;
|
||||||
|
private const int PltMemorySize = 1024 * 1024; // 1MB for stubs
|
||||||
|
|
||||||
|
public StubManager()
|
||||||
|
{
|
||||||
|
_pltMemory = (byte*)VirtualAlloc(
|
||||||
|
null,
|
||||||
|
(nuint)PltMemorySize,
|
||||||
|
AllocationType.Reserve | AllocationType.Commit,
|
||||||
|
MemoryProtection.ExecuteReadWrite);
|
||||||
|
|
||||||
|
if (_pltMemory == null)
|
||||||
|
{
|
||||||
|
throw new OutOfMemoryException("Failed to allocate executable memory for stubs");
|
||||||
|
}
|
||||||
|
|
||||||
|
_pltOffset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nint CreateImportStub(ulong guestAddress, string nid, ImportHandler handler)
|
||||||
|
{
|
||||||
|
if (!_importHandlers.TryGetValue(nid, out var handlerPtr))
|
||||||
|
{
|
||||||
|
handlerPtr = CreateHandlerTrampoline(nid, handler);
|
||||||
|
_importHandlers[nid] = handlerPtr;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stubPtr = _pltMemory + _pltOffset;
|
||||||
|
var stubIndex = (uint)(_stubAddresses.Count);
|
||||||
|
|
||||||
|
JitStubs.CreateJmpWithIndex(stubPtr, stubIndex, (void*)handlerPtr);
|
||||||
|
|
||||||
|
_pltOffset += JitStubs.JmpWithIndex.Size;
|
||||||
|
_stubAddresses[guestAddress] = (nint)stubPtr;
|
||||||
|
|
||||||
|
return (nint)stubPtr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nint CreatePltEntry(uint index, nint gotAddress)
|
||||||
|
{
|
||||||
|
var pltPtr = _pltMemory + _pltOffset;
|
||||||
|
|
||||||
|
var pltCode = new byte[]
|
||||||
|
{
|
||||||
|
0x49, 0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // movabs r11,<addr>
|
||||||
|
0x41, 0xFF, 0x73, 0x08, // push QWORD PTR [r11+8]
|
||||||
|
0x41, 0xFF, 0x63, 0x10, // jmp QWORD PTR [r11+16]
|
||||||
|
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 // nop padding
|
||||||
|
};
|
||||||
|
|
||||||
|
*(ulong*)(pltCode.AsSpan().Slice(2).GetPinnableReference()) = (ulong)gotAddress;
|
||||||
|
|
||||||
|
fixed (byte* src = pltCode)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(src, pltPtr, pltCode.Length, pltCode.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
_pltOffset += pltCode.Length;
|
||||||
|
|
||||||
|
return (nint)pltPtr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nint CreateTlsHandler(delegate*<void> tlsGetAddrFunc)
|
||||||
|
{
|
||||||
|
var handlerPtr = _pltMemory + _pltOffset;
|
||||||
|
|
||||||
|
JitStubs.CreateCall9(handlerPtr, (void*)tlsGetAddrFunc);
|
||||||
|
|
||||||
|
_pltOffset += JitStubs.Call9.Size;
|
||||||
|
|
||||||
|
return (nint)handlerPtr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nint CreateSafeCallTrampoline(delegate*<void> func, byte* regSaveArea, byte* lockVar)
|
||||||
|
{
|
||||||
|
var trampolinePtr = _pltMemory + _pltOffset;
|
||||||
|
|
||||||
|
var template = JitStubs.SafeCall.Template;
|
||||||
|
fixed (byte* src = template)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(src, trampolinePtr, template.Length, template.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
*(ulong*)(trampolinePtr + 0x0c + 2) = (ulong)lockVar;
|
||||||
|
|
||||||
|
*(ulong*)(trampolinePtr + 0x16 + 2) = (ulong)regSaveArea;
|
||||||
|
|
||||||
|
*(ulong*)(trampolinePtr + 0x20 + 2) = (ulong)func;
|
||||||
|
|
||||||
|
_pltOffset += template.Length;
|
||||||
|
|
||||||
|
return (nint)trampolinePtr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetStubAddress(ulong guestAddress, out nint hostAddress)
|
||||||
|
{
|
||||||
|
return _stubAddresses.TryGetValue(guestAddress, out hostAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool PatchTlsAccess(byte* guestCode, int codeSize, nint tlsHandler)
|
||||||
|
{
|
||||||
|
var locations = JitStubs.FindTlsAccessPatterns(guestCode, codeSize);
|
||||||
|
|
||||||
|
foreach (var location in locations)
|
||||||
|
{
|
||||||
|
JitStubs.CreateCall9((byte*)location, (void*)tlsHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
return locations.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private nint CreateHandlerTrampoline(string nid, ImportHandler handler)
|
||||||
|
{
|
||||||
|
var trampolinePtr = _pltMemory + _pltOffset;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var code = new List<byte>();
|
||||||
|
|
||||||
|
code.Add(0x57);
|
||||||
|
code.Add(0x56);
|
||||||
|
code.Add(0x55);
|
||||||
|
code.Add(0x53);
|
||||||
|
code.AddRange(new byte[] { 0x41, 0x54 });
|
||||||
|
code.AddRange(new byte[] { 0x41, 0x55 });
|
||||||
|
code.AddRange(new byte[] { 0x41, 0x56 });
|
||||||
|
code.AddRange(new byte[] { 0x41, 0x57 });
|
||||||
|
|
||||||
|
code.AddRange(new byte[] { 0x48, 0x83, 0xEC, 0x28 });
|
||||||
|
|
||||||
|
int contextOffset = code.Count;
|
||||||
|
code.AddRange(new byte[] { 0x48, 0xB9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
|
||||||
|
|
||||||
|
int handlerOffset = code.Count;
|
||||||
|
code.AddRange(new byte[] { 0x48, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
|
||||||
|
|
||||||
|
code.AddRange(new byte[] { 0xFF, 0xD0 });
|
||||||
|
|
||||||
|
code.AddRange(new byte[] { 0x48, 0x83, 0xC4, 0x28 });
|
||||||
|
|
||||||
|
code.AddRange(new byte[] { 0x41, 0x5F });
|
||||||
|
code.AddRange(new byte[] { 0x41, 0x5E });
|
||||||
|
code.AddRange(new byte[] { 0x41, 0x5D });
|
||||||
|
code.AddRange(new byte[] { 0x41, 0x5C });
|
||||||
|
code.Add(0x5B);
|
||||||
|
code.Add(0x5D);
|
||||||
|
code.Add(0x5E);
|
||||||
|
code.Add(0x5F);
|
||||||
|
|
||||||
|
code.Add(0xC3);
|
||||||
|
|
||||||
|
var codeArray = code.ToArray();
|
||||||
|
fixed (byte* src = codeArray)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(src, trampolinePtr, codeArray.Length, codeArray.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
*(ulong*)(trampolinePtr + contextOffset + 2) = 0;
|
||||||
|
|
||||||
|
var handlerPtr = Marshal.GetFunctionPointerForDelegate(handler);
|
||||||
|
*(ulong*)(trampolinePtr + handlerOffset + 2) = (ulong)handlerPtr;
|
||||||
|
|
||||||
|
_pltOffset += codeArray.Length;
|
||||||
|
|
||||||
|
_allocatedStubs.Add((nint)trampolinePtr);
|
||||||
|
|
||||||
|
return (nint)trampolinePtr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_pltMemory != null)
|
||||||
|
{
|
||||||
|
VirtualFree(_pltMemory, 0, FreeType.Release);
|
||||||
|
_pltMemory = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_allocatedStubs.Clear();
|
||||||
|
_importHandlers.Clear();
|
||||||
|
_stubAddresses.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, AllocationType flAllocationType, MemoryProtection flProtect);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern bool VirtualFree(void* lpAddress, nuint dwSize, FreeType dwFreeType);
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
private enum AllocationType : uint
|
||||||
|
{
|
||||||
|
Commit = 0x1000,
|
||||||
|
Reserve = 0x2000,
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
private enum MemoryProtection : uint
|
||||||
|
{
|
||||||
|
ExecuteReadWrite = 0x40,
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum FreeType : uint
|
||||||
|
{
|
||||||
|
Release = 0x8000,
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate void ImportHandler(CpuContext context);
|
||||||
|
}
|
||||||
11
src/SharpEmu.Core/Cpu/RuntimeStubNids.cs
Normal file
11
src/SharpEmu.Core/Cpu/RuntimeStubNids.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
|
internal static class RuntimeStubNids
|
||||||
|
{
|
||||||
|
public const string BootstrapBridge = "__internal_bootstrap_bridge";
|
||||||
|
|
||||||
|
public const string KernelDynlibDlsym = "__internal_kernel_dynlib_dlsym";
|
||||||
|
}
|
||||||
42
src/SharpEmu.Core/Cpu/TrackedCpuMemory.cs
Normal file
42
src/SharpEmu.Core/Cpu/TrackedCpuMemory.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
|
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory
|
||||||
|
{
|
||||||
|
private readonly ICpuMemory _inner;
|
||||||
|
|
||||||
|
public TrackedCpuMemory(ICpuMemory inner)
|
||||||
|
{
|
||||||
|
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
|
||||||
|
}
|
||||||
|
|
||||||
|
public CpuMemoryAccessFailure? LastFailure { get; private set; }
|
||||||
|
|
||||||
|
public ICpuMemory Inner => _inner;
|
||||||
|
|
||||||
|
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||||
|
{
|
||||||
|
var result = _inner.TryRead(virtualAddress, destination);
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
LastFailure = new CpuMemoryAccessFailure(virtualAddress, destination.Length, isWrite: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||||
|
{
|
||||||
|
var result = _inner.TryWrite(virtualAddress, source);
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
LastFailure = new CpuMemoryAccessFailure(virtualAddress, source.Length, isWrite: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/SharpEmu.Core/IFileSystem.cs
Normal file
11
src/SharpEmu.Core/IFileSystem.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Core;
|
||||||
|
|
||||||
|
public interface IFileSystem
|
||||||
|
{
|
||||||
|
bool Exists(string path);
|
||||||
|
|
||||||
|
bool TryReadAllBytes(string path, out byte[] data);
|
||||||
|
}
|
||||||
76
src/SharpEmu.Core/Loader/ElfHeader.cs
Normal file
76
src/SharpEmu.Core/Loader/ElfHeader.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Loader;
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||||
|
public readonly struct ElfHeader
|
||||||
|
{
|
||||||
|
private readonly byte _ident0;
|
||||||
|
private readonly byte _ident1;
|
||||||
|
private readonly byte _ident2;
|
||||||
|
private readonly byte _ident3;
|
||||||
|
private readonly byte _ident4;
|
||||||
|
private readonly byte _ident5;
|
||||||
|
private readonly byte _ident6;
|
||||||
|
private readonly byte _ident7;
|
||||||
|
private readonly byte _ident8;
|
||||||
|
private readonly byte _ident9;
|
||||||
|
private readonly byte _ident10;
|
||||||
|
private readonly byte _ident11;
|
||||||
|
private readonly byte _ident12;
|
||||||
|
private readonly byte _ident13;
|
||||||
|
private readonly byte _ident14;
|
||||||
|
private readonly byte _ident15;
|
||||||
|
private readonly ushort _type;
|
||||||
|
private readonly ushort _machine;
|
||||||
|
private readonly uint _version;
|
||||||
|
private readonly ulong _entryPoint;
|
||||||
|
private readonly ulong _programHeaderOffset;
|
||||||
|
private readonly ulong _sectionHeaderOffset;
|
||||||
|
private readonly uint _flags;
|
||||||
|
private readonly ushort _headerSize;
|
||||||
|
private readonly ushort _programHeaderEntrySize;
|
||||||
|
private readonly ushort _programHeaderCount;
|
||||||
|
private readonly ushort _sectionHeaderEntrySize;
|
||||||
|
private readonly ushort _sectionHeaderCount;
|
||||||
|
private readonly ushort _sectionHeaderStringIndex;
|
||||||
|
|
||||||
|
public bool HasElfMagic => _ident0 == 0x7F && _ident1 == (byte)'E' && _ident2 == (byte)'L' && _ident3 == (byte)'F';
|
||||||
|
|
||||||
|
public bool Is64Bit => _ident4 == 2;
|
||||||
|
|
||||||
|
public bool IsLittleEndian => _ident5 == 1;
|
||||||
|
|
||||||
|
public byte Abi => _ident7;
|
||||||
|
|
||||||
|
public byte AbiVersion => _ident8;
|
||||||
|
|
||||||
|
public ushort Type => _type;
|
||||||
|
|
||||||
|
public ushort Machine => _machine;
|
||||||
|
|
||||||
|
public uint Version => _version;
|
||||||
|
|
||||||
|
public ulong EntryPoint => _entryPoint;
|
||||||
|
|
||||||
|
public ulong ProgramHeaderOffset => _programHeaderOffset;
|
||||||
|
|
||||||
|
public ulong SectionHeaderOffset => _sectionHeaderOffset;
|
||||||
|
|
||||||
|
public uint Flags => _flags;
|
||||||
|
|
||||||
|
public ushort HeaderSize => _headerSize;
|
||||||
|
|
||||||
|
public ushort ProgramHeaderEntrySize => _programHeaderEntrySize;
|
||||||
|
|
||||||
|
public ushort ProgramHeaderCount => _programHeaderCount;
|
||||||
|
|
||||||
|
public ushort SectionHeaderEntrySize => _sectionHeaderEntrySize;
|
||||||
|
|
||||||
|
public ushort SectionHeaderCount => _sectionHeaderCount;
|
||||||
|
|
||||||
|
public ushort SectionHeaderStringIndex => _sectionHeaderStringIndex;
|
||||||
|
}
|
||||||
21
src/SharpEmu.Core/Loader/ISelfLoader.cs
Normal file
21
src/SharpEmu.Core/Loader/ISelfLoader.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.Core;
|
||||||
|
using SharpEmu.Core.Memory;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Loader;
|
||||||
|
|
||||||
|
public interface ISelfLoader
|
||||||
|
{
|
||||||
|
SelfImage Load(ReadOnlySpan<byte> imageData, IVirtualMemory virtualMemory);
|
||||||
|
|
||||||
|
SelfImage Load(ReadOnlySpan<byte> imageData, IVirtualMemory virtualMemory, IFileSystem? fs, string? mountRoot);
|
||||||
|
|
||||||
|
SelfImage Load(ReadOnlySpan<byte> imageData, IVirtualMemory virtualMemory, IModuleManager moduleManager);
|
||||||
|
|
||||||
|
SelfImage Load(ReadOnlySpan<byte> imageData, IVirtualMemory virtualMemory, IModuleManager moduleManager, IFileSystem? fs, string? mountRoot);
|
||||||
|
|
||||||
|
SelfImage LoadAdditional(ReadOnlySpan<byte> imageData, IVirtualMemory virtualMemory, IModuleManager moduleManager, IFileSystem? fs, string? mountRoot);
|
||||||
|
}
|
||||||
103
src/SharpEmu.Core/Loader/ParamLoader.cs
Normal file
103
src/SharpEmu.Core/Loader/ParamLoader.cs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Text.Json;
|
||||||
|
using SharpEmu.Core;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Loader;
|
||||||
|
|
||||||
|
public sealed record ParamLoader(
|
||||||
|
string? TitleId,
|
||||||
|
string? ContentId,
|
||||||
|
string? ContentVersion,
|
||||||
|
string? MasterVersion,
|
||||||
|
string? TargetContentVersion,
|
||||||
|
LocalizedParameters? LocalizedParameters,
|
||||||
|
Disc? Disc
|
||||||
|
);
|
||||||
|
|
||||||
|
public sealed record LocalizedParameters(
|
||||||
|
string? DefaultLanguage,
|
||||||
|
Dictionary<string, LocalizedLanguage>? Languages
|
||||||
|
);
|
||||||
|
|
||||||
|
public sealed record LocalizedLanguage(string? TitleName);
|
||||||
|
|
||||||
|
public sealed record Disc(LocalizedParameters? LocalizedParameters);
|
||||||
|
|
||||||
|
public static class Ps5ParamJsonReader
|
||||||
|
{
|
||||||
|
public static (string? Title, string? TitleId, string? Version) TryReadPs5Param(IFileSystem fs, string paramJsonPath)
|
||||||
|
{
|
||||||
|
if (!fs.Exists(paramJsonPath))
|
||||||
|
return (null, null, null);
|
||||||
|
|
||||||
|
if (!fs.TryReadAllBytes(paramJsonPath, out var data))
|
||||||
|
return (null, null, null);
|
||||||
|
|
||||||
|
return TryReadPs5Param(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (string? Title, string? TitleId, string? Version) TryReadPs5Param(byte[] data)
|
||||||
|
{
|
||||||
|
if (data == null || data.Length == 0)
|
||||||
|
return (null, null, null);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(data);
|
||||||
|
return TryReadPs5Param(doc.RootElement);
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return (null, null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (string? Title, string? TitleId, string? Version) TryReadPs5Param(JsonElement root)
|
||||||
|
{
|
||||||
|
string? titleId = root.TryGetProperty("titleId", out var eTid) ? eTid.GetString() : null;
|
||||||
|
|
||||||
|
string? ver =
|
||||||
|
(root.TryGetProperty("contentVersion", out var cv) ? cv.GetString() : null)
|
||||||
|
?? (root.TryGetProperty("masterVersion", out var mv) ? mv.GetString() : null)
|
||||||
|
?? (root.TryGetProperty("targetContentVersion", out var tv) ? tv.GetString() : null);
|
||||||
|
|
||||||
|
string? title = ExtractTitleName(root);
|
||||||
|
|
||||||
|
return (title, titleId, ver);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ExtractTitleName(JsonElement root)
|
||||||
|
{
|
||||||
|
if (!root.TryGetProperty("localizedParameters", out var lp))
|
||||||
|
{
|
||||||
|
if (root.TryGetProperty("disc", out var disc) && disc.ValueKind == JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
disc.TryGetProperty("localizedParameters", out lp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lp.ValueKind != JsonValueKind.Object)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
string? defLang = lp.TryGetProperty("defaultLanguage", out var dl) ? dl.GetString() : null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(defLang))
|
||||||
|
{
|
||||||
|
if (lp.TryGetProperty(defLang, out var langObj) && langObj.ValueKind == JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
if (langObj.TryGetProperty("titleName", out var tn))
|
||||||
|
return tn.GetString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lp.TryGetProperty("en-US", out var en) && en.ValueKind == JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
if (en.TryGetProperty("titleName", out var tn2))
|
||||||
|
return tn2.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
70
src/SharpEmu.Core/Loader/ProgramHeader.cs
Normal file
70
src/SharpEmu.Core/Loader/ProgramHeader.cs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Loader;
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||||
|
public readonly struct ProgramHeader
|
||||||
|
{
|
||||||
|
private readonly uint _type;
|
||||||
|
private readonly uint _flags;
|
||||||
|
private readonly ulong _offset;
|
||||||
|
private readonly ulong _virtualAddress;
|
||||||
|
private readonly ulong _physicalAddress;
|
||||||
|
private readonly ulong _fileSize;
|
||||||
|
private readonly ulong _memorySize;
|
||||||
|
private readonly ulong _alignment;
|
||||||
|
|
||||||
|
public uint Type => _type;
|
||||||
|
|
||||||
|
public ProgramHeaderType HeaderType => (ProgramHeaderType)_type;
|
||||||
|
|
||||||
|
public uint RawFlags => _flags;
|
||||||
|
|
||||||
|
public ProgramHeaderFlags Flags => (ProgramHeaderFlags)_flags;
|
||||||
|
|
||||||
|
public ulong Offset => _offset;
|
||||||
|
|
||||||
|
public ulong VirtualAddress => _virtualAddress;
|
||||||
|
|
||||||
|
public ulong PhysicalAddress => _physicalAddress;
|
||||||
|
|
||||||
|
public ulong FileSize => _fileSize;
|
||||||
|
|
||||||
|
public ulong MemorySize => _memorySize;
|
||||||
|
|
||||||
|
public ulong Alignment => _alignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ProgramHeaderType : uint
|
||||||
|
{
|
||||||
|
Null = 0,
|
||||||
|
|
||||||
|
Load = 1,
|
||||||
|
|
||||||
|
Dynamic = 2,
|
||||||
|
|
||||||
|
Tls = 7,
|
||||||
|
|
||||||
|
SceRela = 0x60000000,
|
||||||
|
|
||||||
|
SceProcParam = 0x61000001,
|
||||||
|
|
||||||
|
SceDynLibData = 0x61000000,
|
||||||
|
|
||||||
|
SceRelro = 0x61000010,
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum ProgramHeaderFlags : uint
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
Execute = 0x1,
|
||||||
|
|
||||||
|
Write = 0x2,
|
||||||
|
|
||||||
|
Read = 0x4,
|
||||||
|
}
|
||||||
50
src/SharpEmu.Core/Loader/SelfImage.cs
Normal file
50
src/SharpEmu.Core/Loader/SelfImage.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.Core.Memory;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Loader;
|
||||||
|
|
||||||
|
public sealed class SelfImage
|
||||||
|
{
|
||||||
|
private readonly ulong _imageBase;
|
||||||
|
|
||||||
|
public SelfImage(
|
||||||
|
bool isSelf,
|
||||||
|
ElfHeader elfHeader,
|
||||||
|
IReadOnlyList<ProgramHeader> programHeaders,
|
||||||
|
IReadOnlyList<VirtualMemoryRegion> mappedRegions,
|
||||||
|
IReadOnlyDictionary<ulong, string>? importStubs = null,
|
||||||
|
IReadOnlyDictionary<string, ulong>? runtimeSymbols = null,
|
||||||
|
ulong imageBase = 0,
|
||||||
|
ulong procParamAddress = 0)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(programHeaders);
|
||||||
|
ArgumentNullException.ThrowIfNull(mappedRegions);
|
||||||
|
|
||||||
|
IsSelf = isSelf;
|
||||||
|
ElfHeader = elfHeader;
|
||||||
|
ProgramHeaders = programHeaders;
|
||||||
|
MappedRegions = mappedRegions;
|
||||||
|
ImportStubs = importStubs ?? new Dictionary<ulong, string>();
|
||||||
|
RuntimeSymbols = runtimeSymbols ?? new Dictionary<string, ulong>(StringComparer.Ordinal);
|
||||||
|
_imageBase = imageBase;
|
||||||
|
ProcParamAddress = procParamAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSelf { get; }
|
||||||
|
|
||||||
|
public ElfHeader ElfHeader { get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<ProgramHeader> ProgramHeaders { get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<VirtualMemoryRegion> MappedRegions { get; }
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<ulong, string> ImportStubs { get; }
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, ulong> RuntimeSymbols { get; }
|
||||||
|
|
||||||
|
public ulong EntryPoint => ElfHeader.EntryPoint + _imageBase;
|
||||||
|
|
||||||
|
public ulong ProcParamAddress { get; }
|
||||||
|
}
|
||||||
2028
src/SharpEmu.Core/Loader/SelfLoader.cs
Normal file
2028
src/SharpEmu.Core/Loader/SelfLoader.cs
Normal file
File diff suppressed because it is too large
Load Diff
16
src/SharpEmu.Core/Memory/IVirtualMemory.cs
Normal file
16
src/SharpEmu.Core/Memory/IVirtualMemory.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.Core.Loader;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Memory;
|
||||||
|
|
||||||
|
public interface IVirtualMemory : ICpuMemory
|
||||||
|
{
|
||||||
|
void Clear();
|
||||||
|
|
||||||
|
void Map(ulong virtualAddress, ulong memorySize, ulong fileOffset, ReadOnlySpan<byte> fileData, ProgramHeaderFlags protection);
|
||||||
|
|
||||||
|
IReadOnlyList<VirtualMemoryRegion> SnapshotRegions();
|
||||||
|
}
|
||||||
490
src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs
Normal file
490
src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs
Normal file
@@ -0,0 +1,490 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using SharpEmu.Core.Loader;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Memory;
|
||||||
|
|
||||||
|
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||||
|
{
|
||||||
|
private readonly object _gate = new();
|
||||||
|
private readonly List<MemoryRegion> _regions = new();
|
||||||
|
private bool _disposed;
|
||||||
|
private const ulong PageSize = 0x1000;
|
||||||
|
private const ulong LargeDataReserveThreshold = 0x4000_0000UL; // 1 GiB
|
||||||
|
private const ulong LazyReservePrimeBytes = 0x5000_0000UL; // 1.25 GiB
|
||||||
|
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
||||||
|
|
||||||
|
private const uint MEM_COMMIT = 0x1000;
|
||||||
|
private const uint MEM_RESERVE = 0x2000;
|
||||||
|
private const uint MEM_RELEASE = 0x8000;
|
||||||
|
private const uint PAGE_EXECUTE_READ = 0x20;
|
||||||
|
private const uint PAGE_EXECUTE_READWRITE = 0x40;
|
||||||
|
private const uint PAGE_EXECUTE = 0x10;
|
||||||
|
private const uint PAGE_EXECUTE_WRITECOPY = 0x80;
|
||||||
|
private const uint PAGE_READWRITE = 0x04;
|
||||||
|
private const uint PAGE_READONLY = 0x02;
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool VirtualFree(void* lpAddress, nuint dwSize, uint dwFreeType);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool VirtualProtect(void* lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
private static extern void FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
||||||
|
|
||||||
|
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
||||||
|
{
|
||||||
|
actualAddress = 0;
|
||||||
|
if (size == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
|
||||||
|
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||||
|
var allocationType = MEM_COMMIT | MEM_RESERVE;
|
||||||
|
var result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, allocationType, protection);
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
actualAddress = (ulong)result;
|
||||||
|
if (actualAddress != desiredAddress)
|
||||||
|
{
|
||||||
|
VirtualFree(result, 0, MEM_RELEASE);
|
||||||
|
actualAddress = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_regions.Add(new MemoryRegion
|
||||||
|
{
|
||||||
|
VirtualAddress = actualAddress,
|
||||||
|
Size = alignedSize,
|
||||||
|
IsExecutable = executable,
|
||||||
|
IsReservedOnly = false,
|
||||||
|
Protection = protection
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var allocationKind = executable ? "executable memory" : "data memory";
|
||||||
|
Console.Error.WriteLine($"[VMEM] Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong AllocateAt(ulong desiredAddress, ulong size, bool executable = true, bool allowAlternative = true)
|
||||||
|
{
|
||||||
|
if (size == 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(size), "Size must be greater than zero");
|
||||||
|
|
||||||
|
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
|
||||||
|
|
||||||
|
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||||
|
var allocationType = MEM_COMMIT | MEM_RESERVE;
|
||||||
|
var reservedOnly = false;
|
||||||
|
var preferReserveOnly = !executable && alignedSize >= LargeDataReserveThreshold;
|
||||||
|
|
||||||
|
void* result = null;
|
||||||
|
if (preferReserveOnly)
|
||||||
|
{
|
||||||
|
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
|
||||||
|
if (result == null && allowAlternative)
|
||||||
|
{
|
||||||
|
result = VirtualAlloc(null, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
reservedOnly = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, allocationType, protection);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
if (!allowAlternative)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Failed to allocate exact mapping at 0x{desiredAddress:X16} ({alignedSize} bytes)");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"[VMEM] Could not allocate at 0x{desiredAddress:X16}, trying any address...");
|
||||||
|
result = VirtualAlloc(null, (nuint)alignedSize, allocationType, protection);
|
||||||
|
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
if (!executable)
|
||||||
|
{
|
||||||
|
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
|
||||||
|
if (result == null && allowAlternative)
|
||||||
|
{
|
||||||
|
result = VirtualAlloc(null, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
reservedOnly = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
throw new OutOfMemoryException($"Failed to allocate {alignedSize} bytes of virtual memory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var actualAddress = (ulong)result;
|
||||||
|
|
||||||
|
var lazyPrimeState = "n/a";
|
||||||
|
if (reservedOnly)
|
||||||
|
{
|
||||||
|
var primeBytes = Math.Min(alignedSize, LazyReservePrimeBytes);
|
||||||
|
if (primeBytes != 0)
|
||||||
|
{
|
||||||
|
ulong committedBytes = 0;
|
||||||
|
while (committedBytes < primeBytes)
|
||||||
|
{
|
||||||
|
var remaining = primeBytes - committedBytes;
|
||||||
|
var chunkBytes = Math.Min(remaining, LazyReservePrimeChunkBytes);
|
||||||
|
var commitAddress = (void*)(actualAddress + committedBytes);
|
||||||
|
var committed = VirtualAlloc(commitAddress, (nuint)chunkBytes, MEM_COMMIT, PAGE_READWRITE);
|
||||||
|
if (committed == null)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
committedBytes += chunkBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (committedBytes != 0)
|
||||||
|
{
|
||||||
|
lazyPrimeState = committedBytes == primeBytes
|
||||||
|
? $"ok:{committedBytes:X}"
|
||||||
|
: $"partial:{committedBytes:X}/{primeBytes:X}";
|
||||||
|
Console.Error.WriteLine($"[VMEM] Primed lazy region: 0x{actualAddress:X16} - 0x{actualAddress + committedBytes:X16} ({committedBytes} bytes)");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lazyPrimeState = $"fail:{primeBytes:X}";
|
||||||
|
Console.Error.WriteLine($"[VMEM] Failed to prime lazy region at 0x{actualAddress:X16} ({primeBytes} bytes), continuing with on-demand commit");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lazyPrimeState = "skip:0";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_regions.Add(new MemoryRegion
|
||||||
|
{
|
||||||
|
VirtualAddress = actualAddress,
|
||||||
|
Size = alignedSize,
|
||||||
|
IsExecutable = executable,
|
||||||
|
IsReservedOnly = reservedOnly,
|
||||||
|
Protection = protection
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var allocationKind = reservedOnly
|
||||||
|
? "reserved data memory (lazy commit)"
|
||||||
|
: (executable ? "executable memory" : "data memory");
|
||||||
|
Console.Error.WriteLine($"[VMEM] Allocated {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes) lazy_prime={lazyPrimeState}");
|
||||||
|
|
||||||
|
return actualAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
foreach (var region in _regions)
|
||||||
|
{
|
||||||
|
VirtualFree((void*)region.VirtualAddress, 0, MEM_RELEASE);
|
||||||
|
}
|
||||||
|
_regions.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Map(ulong virtualAddress, ulong memorySize, ulong fileOffset, ReadOnlySpan<byte> fileData, ProgramHeaderFlags protection)
|
||||||
|
{
|
||||||
|
if (memorySize == 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(memorySize));
|
||||||
|
|
||||||
|
if ((ulong)fileData.Length > memorySize)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(fileData), "File size cannot exceed memory size");
|
||||||
|
|
||||||
|
var mapStart = AlignDown(virtualAddress, PageSize);
|
||||||
|
var segmentEnd = checked(virtualAddress + memorySize);
|
||||||
|
var mapEnd = AlignUp(segmentEnd, PageSize);
|
||||||
|
var mapSize = checked(mapEnd - mapStart);
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
var existingRegion = FindRegion(mapStart, mapSize);
|
||||||
|
if (existingRegion == null)
|
||||||
|
{
|
||||||
|
var isExecutable = (protection & ProgramHeaderFlags.Execute) != 0;
|
||||||
|
AllocateAt(mapStart, mapSize, isExecutable, allowAlternative: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var stageProtection = (protection & ProgramHeaderFlags.Execute) != 0
|
||||||
|
? ProgramHeaderFlags.Read | ProgramHeaderFlags.Write | ProgramHeaderFlags.Execute
|
||||||
|
: ProgramHeaderFlags.Read | ProgramHeaderFlags.Write;
|
||||||
|
SetProtection(mapStart, mapSize, stageProtection);
|
||||||
|
|
||||||
|
if (!fileData.IsEmpty)
|
||||||
|
{
|
||||||
|
var destPtr = (void*)virtualAddress;
|
||||||
|
fixed (byte* srcPtr = fileData)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)memorySize, (nuint)fileData.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var zeroFillSize = memorySize - (ulong)fileData.Length;
|
||||||
|
if (zeroFillSize != 0)
|
||||||
|
{
|
||||||
|
NativeMemory.Clear((void*)(virtualAddress + (ulong)fileData.Length), (nuint)zeroFillSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetProtection(mapStart, mapSize, protection);
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"[VMEM] Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetProtection(ulong address, ulong size, ProgramHeaderFlags flags)
|
||||||
|
{
|
||||||
|
uint protection;
|
||||||
|
|
||||||
|
if ((flags & ProgramHeaderFlags.Execute) != 0)
|
||||||
|
{
|
||||||
|
protection = (flags & ProgramHeaderFlags.Write) != 0
|
||||||
|
? PAGE_EXECUTE_READWRITE
|
||||||
|
: PAGE_EXECUTE_READ;
|
||||||
|
}
|
||||||
|
else if ((flags & ProgramHeaderFlags.Write) != 0)
|
||||||
|
{
|
||||||
|
protection = PAGE_READWRITE;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
protection = PAGE_READONLY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!VirtualProtect((void*)address, (nuint)size, protection, out _))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Failed to set memory protection at 0x{address:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((flags & ProgramHeaderFlags.Execute) != 0)
|
||||||
|
{
|
||||||
|
FlushInstructionCache(null, (void*)address, (nuint)size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<VirtualMemoryRegion> SnapshotRegions()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
var snapshot = new VirtualMemoryRegion[_regions.Count];
|
||||||
|
for (var i = 0; i < _regions.Count; i++)
|
||||||
|
{
|
||||||
|
var r = _regions[i];
|
||||||
|
snapshot[i] = new VirtualMemoryRegion(
|
||||||
|
r.VirtualAddress,
|
||||||
|
r.Size,
|
||||||
|
0,
|
||||||
|
r.Size,
|
||||||
|
r.IsExecutable ? ProgramHeaderFlags.Execute | ProgramHeaderFlags.Read : ProgramHeaderFlags.Read);
|
||||||
|
}
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
foreach (var region in _regions)
|
||||||
|
{
|
||||||
|
if (TryResolveRegionOffset(virtualAddress, (ulong)destination.Length, region, out var offset))
|
||||||
|
{
|
||||||
|
var srcPtr = (void*)(region.VirtualAddress + offset);
|
||||||
|
fixed (byte* destPtr = destination)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
foreach (var region in _regions)
|
||||||
|
{
|
||||||
|
if (TryResolveRegionOffset(virtualAddress, (ulong)source.Length, region, out var offset))
|
||||||
|
{
|
||||||
|
var destPtr = (void*)(region.VirtualAddress + offset);
|
||||||
|
if (source.IsEmpty)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fixed (byte* srcPtr = source)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
|
||||||
|
if (IsExecutableProtection(oldProtect))
|
||||||
|
{
|
||||||
|
FlushInstructionCache(null, destPtr, (nuint)source.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryWriteUInt64(ulong virtualAddress, ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
|
BitConverter.TryWriteBytes(buffer, value);
|
||||||
|
return TryWrite(virtualAddress, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void* GetPointer(ulong virtualAddress)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
foreach (var region in _regions)
|
||||||
|
{
|
||||||
|
if (virtualAddress >= region.VirtualAddress &&
|
||||||
|
virtualAddress < region.VirtualAddress + region.Size)
|
||||||
|
{
|
||||||
|
return (void*)virtualAddress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsAccessible(ulong virtualAddress, ulong size)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
foreach (var region in _regions)
|
||||||
|
{
|
||||||
|
if (TryResolveRegionOffset(virtualAddress, size, region, out _))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MemoryRegion? FindRegion(ulong address, ulong size)
|
||||||
|
{
|
||||||
|
foreach (var region in _regions)
|
||||||
|
{
|
||||||
|
if (TryResolveRegionOffset(address, size, region, out _))
|
||||||
|
{
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryResolveRegionOffset(ulong address, ulong size, MemoryRegion region, out ulong offset)
|
||||||
|
{
|
||||||
|
offset = 0;
|
||||||
|
if (address < region.VirtualAddress)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset = address - region.VirtualAddress;
|
||||||
|
if (offset > region.Size)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size > region.Size - offset)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsExecutableProtection(uint protection)
|
||||||
|
{
|
||||||
|
return protection is PAGE_EXECUTE or PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE or PAGE_EXECUTE_WRITECOPY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong AlignDown(ulong value, ulong alignment)
|
||||||
|
{
|
||||||
|
var mask = alignment - 1;
|
||||||
|
return value & ~mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong AlignUp(ulong value, ulong alignment)
|
||||||
|
{
|
||||||
|
var mask = alignment - 1;
|
||||||
|
return checked((value + mask) & ~mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
Clear();
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MemoryRegion
|
||||||
|
{
|
||||||
|
public ulong VirtualAddress { get; set; }
|
||||||
|
public ulong Size { get; set; }
|
||||||
|
public bool IsExecutable { get; set; }
|
||||||
|
public bool IsReservedOnly { get; set; }
|
||||||
|
public uint Protection { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
127
src/SharpEmu.Core/Memory/VirtualMemory.cs
Normal file
127
src/SharpEmu.Core/Memory/VirtualMemory.cs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.Core.Loader;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Memory;
|
||||||
|
|
||||||
|
public sealed class VirtualMemory : IVirtualMemory
|
||||||
|
{
|
||||||
|
private readonly object _gate = new();
|
||||||
|
private readonly List<MappedRegion> _regions = new();
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_regions.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Map(ulong virtualAddress, ulong memorySize, ulong fileOffset, ReadOnlySpan<byte> fileData, ProgramHeaderFlags protection)
|
||||||
|
{
|
||||||
|
if (memorySize == 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(memorySize), "Memory size must be greater than zero.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((ulong)fileData.Length > memorySize)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(fileData), "File size cannot exceed memory size.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memorySize > int.MaxValue)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Virtual memory regions larger than 2 GB are not currently supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var endAddress = checked(virtualAddress + memorySize);
|
||||||
|
var backingMemory = new byte[(int)memorySize];
|
||||||
|
fileData.CopyTo(backingMemory);
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
foreach (var existing in _regions)
|
||||||
|
{
|
||||||
|
if (virtualAddress < existing.EndAddress && endAddress > existing.Region.VirtualAddress)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Attempted to map an overlapping virtual memory region.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_regions.Add(new MappedRegion(
|
||||||
|
new VirtualMemoryRegion(virtualAddress, memorySize, fileOffset, (ulong)fileData.Length, protection),
|
||||||
|
endAddress,
|
||||||
|
backingMemory));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<VirtualMemoryRegion> SnapshotRegions()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
var snapshot = new VirtualMemoryRegion[_regions.Count];
|
||||||
|
for (var i = 0; i < _regions.Count; i++)
|
||||||
|
{
|
||||||
|
snapshot[i] = _regions[i].Region;
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!TryResolveRegion(virtualAddress, destination.Length, out var region, out var offset))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
region.BackingMemory.AsSpan(offset, destination.Length).CopyTo(destination);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!TryResolveRegion(virtualAddress, source.Length, out var region, out var offset))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
source.CopyTo(region.BackingMemory.AsSpan(offset, source.Length));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryResolveRegion(ulong virtualAddress, int length, out MappedRegion region, out int offset)
|
||||||
|
{
|
||||||
|
foreach (var candidate in _regions)
|
||||||
|
{
|
||||||
|
if (virtualAddress < candidate.Region.VirtualAddress || virtualAddress >= candidate.EndAddress)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidateOffset = checked((int)(virtualAddress - candidate.Region.VirtualAddress));
|
||||||
|
if (candidateOffset + length > candidate.BackingMemory.Length)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
region = candidate;
|
||||||
|
offset = candidateOffset;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
region = default;
|
||||||
|
offset = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct MappedRegion(VirtualMemoryRegion Region, ulong EndAddress, byte[] BackingMemory);
|
||||||
|
}
|
||||||
33
src/SharpEmu.Core/Memory/VirtualMemoryRegion.cs
Normal file
33
src/SharpEmu.Core/Memory/VirtualMemoryRegion.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.Core.Loader;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Memory;
|
||||||
|
|
||||||
|
public readonly struct VirtualMemoryRegion
|
||||||
|
{
|
||||||
|
public VirtualMemoryRegion(
|
||||||
|
ulong virtualAddress,
|
||||||
|
ulong memorySize,
|
||||||
|
ulong fileOffset,
|
||||||
|
ulong fileSize,
|
||||||
|
ProgramHeaderFlags protection)
|
||||||
|
{
|
||||||
|
VirtualAddress = virtualAddress;
|
||||||
|
MemorySize = memorySize;
|
||||||
|
FileOffset = fileOffset;
|
||||||
|
FileSize = fileSize;
|
||||||
|
Protection = protection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong VirtualAddress { get; }
|
||||||
|
|
||||||
|
public ulong MemorySize { get; }
|
||||||
|
|
||||||
|
public ulong FileOffset { get; }
|
||||||
|
|
||||||
|
public ulong FileSize { get; }
|
||||||
|
|
||||||
|
public ProgramHeaderFlags Protection { get; }
|
||||||
|
}
|
||||||
37
src/SharpEmu.Core/PhysicalFileSystem.cs
Normal file
37
src/SharpEmu.Core/PhysicalFileSystem.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Core;
|
||||||
|
|
||||||
|
public sealed class PhysicalFileSystem : IFileSystem
|
||||||
|
{
|
||||||
|
public bool Exists(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return File.Exists(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryReadAllBytes(string path, out byte[] data)
|
||||||
|
{
|
||||||
|
data = Array.Empty<byte>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!File.Exists(path))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
data = File.ReadAllBytes(path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
data = Array.Empty<byte>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/SharpEmu.Core/Runtime/ISharpEmuRuntime.cs
Normal file
26
src/SharpEmu.Core/Runtime/ISharpEmuRuntime.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.Core.Loader;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Runtime;
|
||||||
|
|
||||||
|
public interface ISharpEmuRuntime : IDisposable
|
||||||
|
{
|
||||||
|
string? LastExecutionDiagnostics { get; }
|
||||||
|
|
||||||
|
string? LastExecutionTrace { get; }
|
||||||
|
|
||||||
|
string? LastSessionSummary { get; }
|
||||||
|
|
||||||
|
string? LastBasicBlockTrace { get; }
|
||||||
|
|
||||||
|
string? LastMilestoneLog { get; }
|
||||||
|
|
||||||
|
SelfImage LoadImage(string ebootPath);
|
||||||
|
|
||||||
|
OrbisGen2Result Run(string ebootPath);
|
||||||
|
|
||||||
|
OrbisGen2Result DispatchHleCall(string nid, CpuContext context);
|
||||||
|
}
|
||||||
713
src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs
Normal file
713
src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs
Normal file
@@ -0,0 +1,713 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.Core;
|
||||||
|
using SharpEmu.Core.Cpu;
|
||||||
|
using SharpEmu.Core.Cpu.Disasm;
|
||||||
|
using SharpEmu.Core.Loader;
|
||||||
|
using SharpEmu.Core.Memory;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.VideoOut;
|
||||||
|
using SharpEmu.Libs.Kernel;
|
||||||
|
using SharpEmu.Libs.AppContent;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Runtime;
|
||||||
|
|
||||||
|
public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||||
|
{
|
||||||
|
private static readonly HashSet<string> PreloadSkipModules = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"libkernel.prx",
|
||||||
|
"libkernel_sys.prx",
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly ISelfLoader _selfLoader;
|
||||||
|
private readonly IVirtualMemory _virtualMemory;
|
||||||
|
private readonly ICpuDispatcher _cpuDispatcher;
|
||||||
|
private readonly IModuleManager _moduleManager;
|
||||||
|
private readonly ISymbolCatalog _symbolCatalog;
|
||||||
|
private readonly CpuExecutionOptions _cpuExecutionOptions;
|
||||||
|
private readonly IFileSystem _fileSystem;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public string? LastExecutionDiagnostics { get; private set; }
|
||||||
|
|
||||||
|
public string? LastExecutionTrace { get; private set; }
|
||||||
|
|
||||||
|
public string? LastSessionSummary { get; private set; }
|
||||||
|
|
||||||
|
public string? LastBasicBlockTrace { get; private set; }
|
||||||
|
|
||||||
|
public string? LastMilestoneLog { get; private set; }
|
||||||
|
|
||||||
|
public SharpEmuRuntime(
|
||||||
|
ISelfLoader selfLoader,
|
||||||
|
IVirtualMemory virtualMemory,
|
||||||
|
ICpuDispatcher cpuDispatcher,
|
||||||
|
IModuleManager moduleManager,
|
||||||
|
ISymbolCatalog? symbolCatalog = null,
|
||||||
|
CpuExecutionOptions cpuExecutionOptions = default,
|
||||||
|
IFileSystem? fileSystem = null)
|
||||||
|
{
|
||||||
|
_selfLoader = selfLoader ?? throw new ArgumentNullException(nameof(selfLoader));
|
||||||
|
_virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory));
|
||||||
|
_cpuDispatcher = cpuDispatcher ?? throw new ArgumentNullException(nameof(cpuDispatcher));
|
||||||
|
_moduleManager = moduleManager ?? throw new ArgumentNullException(nameof(moduleManager));
|
||||||
|
_symbolCatalog = symbolCatalog ?? Aerolib.Empty;
|
||||||
|
_cpuExecutionOptions = new CpuExecutionOptions
|
||||||
|
{
|
||||||
|
CpuEngine = cpuExecutionOptions.CpuEngine,
|
||||||
|
StrictDynlibResolution = cpuExecutionOptions.StrictDynlibResolution,
|
||||||
|
ImportTraceLimit = Math.Max(0, cpuExecutionOptions.ImportTraceLimit),
|
||||||
|
};
|
||||||
|
_fileSystem = fileSystem ?? new PhysicalFileSystem();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ISharpEmuRuntime CreateDefault(SharpEmuRuntimeOptions options = default)
|
||||||
|
{
|
||||||
|
var cpuExecutionOptions = new CpuExecutionOptions
|
||||||
|
{
|
||||||
|
CpuEngine = options.CpuEngine,
|
||||||
|
StrictDynlibResolution = options.StrictDynlibResolution,
|
||||||
|
ImportTraceLimit = Math.Max(0, options.ImportTraceLimit),
|
||||||
|
};
|
||||||
|
var moduleManager = new ModuleManager();
|
||||||
|
moduleManager.RegisterFromAssembly(typeof(VideoOutExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
|
||||||
|
moduleManager.RegisterFromAssembly(typeof(KernelExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
|
||||||
|
moduleManager.Freeze();
|
||||||
|
|
||||||
|
var virtualMemory = new PhysicalVirtualMemory();
|
||||||
|
|
||||||
|
var fileSystem = new PhysicalFileSystem();
|
||||||
|
|
||||||
|
return new SharpEmuRuntime(
|
||||||
|
new SelfLoader(),
|
||||||
|
virtualMemory,
|
||||||
|
new CpuDispatcher(virtualMemory, moduleManager),
|
||||||
|
moduleManager,
|
||||||
|
Aerolib.Instance,
|
||||||
|
cpuExecutionOptions,
|
||||||
|
fileSystem);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SelfImage LoadImage(string ebootPath)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(ebootPath);
|
||||||
|
|
||||||
|
var fullPath = Path.GetFullPath(ebootPath);
|
||||||
|
if (!File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException("Executable file was not found.", fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileInfo = new FileInfo(fullPath);
|
||||||
|
if (fileInfo.Length > int.MaxValue)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Images larger than 2 GB are not currently supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = GC.AllocateUninitializedArray<byte>((int)fileInfo.Length);
|
||||||
|
using (var stream = File.OpenRead(fullPath))
|
||||||
|
{
|
||||||
|
stream.ReadExactly(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
var mountRoot = Path.GetDirectoryName(fullPath);
|
||||||
|
|
||||||
|
return _selfLoader.Load(bytes.AsSpan(), _virtualMemory, _moduleManager, _fileSystem, mountRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OrbisGen2Result Run(string ebootPath)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}");
|
||||||
|
LastExecutionDiagnostics = null;
|
||||||
|
LastExecutionTrace = null;
|
||||||
|
LastSessionSummary = null;
|
||||||
|
LastBasicBlockTrace = null;
|
||||||
|
LastMilestoneLog = null;
|
||||||
|
KernelModuleRegistry.Reset();
|
||||||
|
var image = LoadImage(ebootPath);
|
||||||
|
var normalizedEbootPath = Path.GetFullPath(ebootPath);
|
||||||
|
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
||||||
|
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
||||||
|
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
|
||||||
|
var generation = image.ElfHeader.AbiVersion == 2 ? Generation.Gen5 : Generation.Gen4;
|
||||||
|
var activeImportStubs = new Dictionary<ulong, string>(image.ImportStubs);
|
||||||
|
var activeRuntimeSymbols = new Dictionary<string, ulong>(image.RuntimeSymbols, StringComparer.Ordinal);
|
||||||
|
LoadAdjacentSceModules(ebootPath, activeImportStubs, activeRuntimeSymbols);
|
||||||
|
var processImageName = Path.GetFileName(ebootPath);
|
||||||
|
if (string.IsNullOrWhiteSpace(processImageName))
|
||||||
|
{
|
||||||
|
processImageName = "eboot.bin";
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"[RUNTIME] Dispatching, gen: {generation}");
|
||||||
|
Console.Error.WriteLine($"[RUNTIME] About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
||||||
|
|
||||||
|
var result = _cpuDispatcher.DispatchEntry(
|
||||||
|
image.EntryPoint,
|
||||||
|
generation,
|
||||||
|
activeImportStubs,
|
||||||
|
activeRuntimeSymbols,
|
||||||
|
processImageName,
|
||||||
|
_cpuExecutionOptions);
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"[RUNTIME] DispatchEntry returned: {result}");
|
||||||
|
Console.Error.WriteLine($"[RUNTIME] Dispatch result: {result}");
|
||||||
|
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||||
|
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||||
|
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||||
|
LastBasicBlockTrace = _cpuDispatcher.LastBasicBlockTrace;
|
||||||
|
if (result == OrbisGen2Result.ORBIS_GEN2_ERROR_CPU_TRAP && _cpuDispatcher.LastTrapInfo is { } trapInfo)
|
||||||
|
{
|
||||||
|
var opcodeBytes = ReadOpcodePreview(trapInfo.InstructionPointer, 8);
|
||||||
|
var decodedTrapText = string.Empty;
|
||||||
|
var ud2Hint = string.Empty;
|
||||||
|
if (_cpuExecutionOptions.EnableDisasmDiagnostics && TryDecodeInstructionAt(trapInfo.InstructionPointer, out var trapInstruction))
|
||||||
|
{
|
||||||
|
decodedTrapText = BuildDecodedInstructionFields(in trapInstruction);
|
||||||
|
if (string.Equals(trapInstruction.Mnemonic, "Ud2", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
ud2Hint = ", trap=ud2";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (opcodeBytes.StartsWith("0F 0B", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
ud2Hint = ", trap=ud2";
|
||||||
|
}
|
||||||
|
|
||||||
|
var longModeHint = IsInvalidLongModeOpcode(trapInfo.Opcode)
|
||||||
|
? ", hint=invalid opcode for x64 long mode; likely wrong jump target or decode desync"
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
|
var hint = string.Empty;
|
||||||
|
if (image.IsSelf &&
|
||||||
|
activeImportStubs.Count == 0 &&
|
||||||
|
trapInfo.InstructionPointer == 0 &&
|
||||||
|
trapInfo.Opcode == 0xCC)
|
||||||
|
{
|
||||||
|
hint = ", hint=SELF appears encrypted or unresolved; use a decrypted ELF/FSELF image";
|
||||||
|
}
|
||||||
|
|
||||||
|
var transferText = string.Empty;
|
||||||
|
if (_cpuDispatcher.LastControlTransferInfo is { } transferInfo)
|
||||||
|
{
|
||||||
|
var transferMode = transferInfo.IsIndirect ? "indirect" : "direct";
|
||||||
|
var transferBytes = ReadOpcodePreview(transferInfo.SourceInstructionPointer, 8);
|
||||||
|
var transferDecodedText = TryDecodeInstructionAt(transferInfo.SourceInstructionPointer, out var transferInstruction)
|
||||||
|
? BuildDecodedInstructionFields(in transferInstruction, fieldPrefix: "src_inst")
|
||||||
|
: string.Empty;
|
||||||
|
transferText =
|
||||||
|
$", last_transfer={transferInfo.Kind}:{transferMode} src=0x{transferInfo.SourceInstructionPointer:X16} op=0x{transferInfo.Opcode:X2} bytes={transferBytes} dst=0x{transferInfo.TargetInstructionPointer:X16}{transferDecodedText}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var ripStubText = activeImportStubs.TryGetValue(trapInfo.InstructionPointer, out var trapStubNid)
|
||||||
|
? $", rip_stub={trapStubNid}"
|
||||||
|
: string.Empty;
|
||||||
|
var diagnosticsBuilder = new StringBuilder(1024);
|
||||||
|
diagnosticsBuilder.Append(
|
||||||
|
$"CPU trap at RIP=0x{trapInfo.InstructionPointer:X16}, opcode=0x{trapInfo.Opcode:X2}, bytes={opcodeBytes}{decodedTrapText}, import_stubs={activeImportStubs.Count}{ud2Hint}{longModeHint}{hint}{ripStubText}{transferText}");
|
||||||
|
if (!string.IsNullOrWhiteSpace(_cpuDispatcher.LastRecentControlTransferTrace))
|
||||||
|
{
|
||||||
|
diagnosticsBuilder.AppendLine();
|
||||||
|
diagnosticsBuilder.Append("Recent transfers:");
|
||||||
|
diagnosticsBuilder.AppendLine();
|
||||||
|
diagnosticsBuilder.Append(_cpuDispatcher.LastRecentControlTransferTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_cpuDispatcher.LastRecentInstructionWindow))
|
||||||
|
{
|
||||||
|
diagnosticsBuilder.AppendLine();
|
||||||
|
diagnosticsBuilder.Append("Recent instructions:");
|
||||||
|
diagnosticsBuilder.AppendLine();
|
||||||
|
diagnosticsBuilder.Append(_cpuDispatcher.LastRecentInstructionWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
LastExecutionDiagnostics = diagnosticsBuilder.ToString();
|
||||||
|
}
|
||||||
|
else if (result == OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT && _cpuDispatcher.LastMemoryFaultInfo is { } faultInfo)
|
||||||
|
{
|
||||||
|
var opcodeText = faultInfo.Opcode.HasValue ? $"0x{faultInfo.Opcode.Value:X2}" : "??";
|
||||||
|
var decodedFaultText = string.Empty;
|
||||||
|
if (_cpuExecutionOptions.EnableDisasmDiagnostics && TryDecodeInstructionAt(faultInfo.InstructionPointer, out var faultInstruction))
|
||||||
|
{
|
||||||
|
decodedFaultText = BuildDecodedInstructionFields(in faultInstruction);
|
||||||
|
if (!faultInfo.Opcode.HasValue && faultInstruction.Bytes.Length > 0)
|
||||||
|
{
|
||||||
|
opcodeText = $"0x{faultInstruction.Bytes[0]:X2}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var accessType = faultInfo.Access.IsWrite ? "write" : "read";
|
||||||
|
var transferText = string.Empty;
|
||||||
|
if (_cpuDispatcher.LastControlTransferInfo is { } transferInfo)
|
||||||
|
{
|
||||||
|
var transferMode = transferInfo.IsIndirect ? "indirect" : "direct";
|
||||||
|
var transferBytes = ReadOpcodePreview(transferInfo.SourceInstructionPointer, 8);
|
||||||
|
var transferDecodedText = TryDecodeInstructionAt(transferInfo.SourceInstructionPointer, out var transferInstruction)
|
||||||
|
? BuildDecodedInstructionFields(in transferInstruction, fieldPrefix: "src_inst")
|
||||||
|
: string.Empty;
|
||||||
|
var rbxTarget = TryReadUInt64At(transferInfo.Rbx, out var rbxDeref)
|
||||||
|
? $"*rbx=0x{rbxDeref:X16}"
|
||||||
|
: "*rbx=??";
|
||||||
|
transferText =
|
||||||
|
$", last_transfer={transferInfo.Kind}:{transferMode} src=0x{transferInfo.SourceInstructionPointer:X16} op=0x{transferInfo.Opcode:X2} bytes={transferBytes} dst=0x{transferInfo.TargetInstructionPointer:X16}{transferDecodedText} rax=0x{transferInfo.Rax:X16} rbx=0x{transferInfo.Rbx:X16} {rbxTarget} rsp=0x{transferInfo.Rsp:X16} rbp=0x{transferInfo.Rbp:X16}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var ripStubText = activeImportStubs.TryGetValue(faultInfo.InstructionPointer, out var faultStubNid)
|
||||||
|
? $", rip_stub={faultStubNid}"
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
|
LastExecutionDiagnostics =
|
||||||
|
$"Memory fault at RIP=0x{faultInfo.InstructionPointer:X16}, opcode={opcodeText}{decodedFaultText}, {accessType}@0x{faultInfo.Access.Address:X16} size={faultInfo.Access.Size}, import_stubs={activeImportStubs.Count}{ripStubText}{transferText}";
|
||||||
|
}
|
||||||
|
else if (result == OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED && _cpuDispatcher.LastNotImplementedInfo is { } notImplementedInfo)
|
||||||
|
{
|
||||||
|
var inferredNid = notImplementedInfo.Nid;
|
||||||
|
var decodedNotImplementedText = TryDecodeInstructionAt(notImplementedInfo.InstructionPointer, out var notImplementedInstruction)
|
||||||
|
? BuildDecodedInstructionFields(in notImplementedInstruction)
|
||||||
|
: string.Empty;
|
||||||
|
var ripStubText = string.Empty;
|
||||||
|
if (activeImportStubs.TryGetValue(notImplementedInfo.InstructionPointer, out var ripStubNid))
|
||||||
|
{
|
||||||
|
ripStubText = $", rip_stub={ripStubNid}";
|
||||||
|
if (string.IsNullOrWhiteSpace(inferredNid))
|
||||||
|
{
|
||||||
|
inferredNid = ripStubNid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var inferredExportName = notImplementedInfo.ExportName;
|
||||||
|
var inferredLibraryName = notImplementedInfo.LibraryName;
|
||||||
|
if (!string.IsNullOrWhiteSpace(inferredNid) &&
|
||||||
|
_moduleManager.TryGetExport(inferredNid, out var export))
|
||||||
|
{
|
||||||
|
inferredExportName = string.IsNullOrWhiteSpace(inferredExportName) ? export.Name : inferredExportName;
|
||||||
|
inferredLibraryName = string.IsNullOrWhiteSpace(inferredLibraryName) ? export.LibraryName : inferredLibraryName;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nidText = string.IsNullOrWhiteSpace(inferredNid) ? "?" : inferredNid;
|
||||||
|
var exportText = string.IsNullOrWhiteSpace(inferredExportName) ? "?" : inferredExportName;
|
||||||
|
var libraryText = string.IsNullOrWhiteSpace(inferredLibraryName) ? "?" : inferredLibraryName;
|
||||||
|
var detailText = string.IsNullOrWhiteSpace(notImplementedInfo.Detail) ? string.Empty : $", detail={notImplementedInfo.Detail}";
|
||||||
|
var transferText = string.Empty;
|
||||||
|
if (_cpuDispatcher.LastControlTransferInfo is { } transferInfo)
|
||||||
|
{
|
||||||
|
var transferMode = transferInfo.IsIndirect ? "indirect" : "direct";
|
||||||
|
var transferBytes = ReadOpcodePreview(transferInfo.SourceInstructionPointer, 8);
|
||||||
|
var transferDecodedText = TryDecodeInstructionAt(transferInfo.SourceInstructionPointer, out var transferInstruction)
|
||||||
|
? BuildDecodedInstructionFields(in transferInstruction, fieldPrefix: "src_inst")
|
||||||
|
: string.Empty;
|
||||||
|
var transferStubText = activeImportStubs.TryGetValue(transferInfo.TargetInstructionPointer, out var transferStubNid)
|
||||||
|
? $" stub={transferStubNid}"
|
||||||
|
: string.Empty;
|
||||||
|
transferText =
|
||||||
|
$", last_transfer={transferInfo.Kind}:{transferMode} src=0x{transferInfo.SourceInstructionPointer:X16} op=0x{transferInfo.Opcode:X2} bytes={transferBytes} dst=0x{transferInfo.TargetInstructionPointer:X16}{transferDecodedText}{transferStubText}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var aerolibText = string.Empty;
|
||||||
|
if (!string.IsNullOrWhiteSpace(inferredExportName) &&
|
||||||
|
_symbolCatalog.TryGetByExportName(inferredExportName, out var symbol))
|
||||||
|
{
|
||||||
|
aerolibText = $", aerolib_nid={symbol.Nid}";
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrWhiteSpace(inferredNid) &&
|
||||||
|
_symbolCatalog.TryGetByNid(inferredNid, out var symbolByNid))
|
||||||
|
{
|
||||||
|
aerolibText = $", aerolib_export={symbolByNid.ExportName}";
|
||||||
|
}
|
||||||
|
|
||||||
|
LastExecutionDiagnostics =
|
||||||
|
$"Not implemented: source={notImplementedInfo.Source}, rip=0x{notImplementedInfo.InstructionPointer:X16}{decodedNotImplementedText}, nid={nidText}, export={exportText}, library={libraryText}, import_stubs={activeImportStubs.Count}{ripStubText}{aerolibText}{detailText}{transferText}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadAdjacentSceModules(
|
||||||
|
string ebootPath,
|
||||||
|
IDictionary<ulong, string> importStubs,
|
||||||
|
IDictionary<string, ulong> runtimeSymbols)
|
||||||
|
{
|
||||||
|
var ebootDirectory = Path.GetDirectoryName(ebootPath);
|
||||||
|
if (string.IsNullOrWhiteSpace(ebootDirectory))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var moduleDirectories = new[]
|
||||||
|
{
|
||||||
|
Path.Combine(ebootDirectory, "sce_module"),
|
||||||
|
Path.Combine(ebootDirectory, "sce_modules"),
|
||||||
|
}
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Where(Directory.Exists)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (moduleDirectories.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var allModulePaths = moduleDirectories
|
||||||
|
.SelectMany(Directory.EnumerateFiles)
|
||||||
|
.Where(path =>
|
||||||
|
{
|
||||||
|
var extension = Path.GetExtension(path);
|
||||||
|
return string.Equals(extension, ".prx", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(extension, ".sprx", StringComparison.OrdinalIgnoreCase);
|
||||||
|
})
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var modulePaths = allModulePaths
|
||||||
|
.Where(ShouldPreloadModule)
|
||||||
|
.ToArray();
|
||||||
|
var skippedModules = allModulePaths
|
||||||
|
.Where(path => !ShouldPreloadModule(path))
|
||||||
|
.Select(Path.GetFileName)
|
||||||
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||||
|
.ToArray();
|
||||||
|
if (skippedModules.Length > 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[RUNTIME] Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modulePaths.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"[RUNTIME] Module search directories: {string.Join(", ", moduleDirectories)}");
|
||||||
|
Console.Error.WriteLine($"[RUNTIME] Loading {modulePaths.Length} module(s)...");
|
||||||
|
var loadedModules = 0;
|
||||||
|
var failedModules = 0;
|
||||||
|
var mergedImportCount = 0;
|
||||||
|
var mergedSymbolCount = 0;
|
||||||
|
foreach (var modulePath in modulePaths)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fileInfo = new FileInfo(modulePath);
|
||||||
|
if (!fileInfo.Exists || fileInfo.Length <= 0 || fileInfo.Length > int.MaxValue)
|
||||||
|
{
|
||||||
|
failedModules++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var moduleBytes = GC.AllocateUninitializedArray<byte>((int)fileInfo.Length);
|
||||||
|
using (var stream = File.OpenRead(modulePath))
|
||||||
|
{
|
||||||
|
stream.ReadExactly(moduleBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
var moduleImage = _selfLoader.LoadAdditional(
|
||||||
|
moduleBytes.AsSpan(),
|
||||||
|
_virtualMemory,
|
||||||
|
_moduleManager,
|
||||||
|
_fileSystem,
|
||||||
|
Path.GetDirectoryName(modulePath));
|
||||||
|
|
||||||
|
mergedImportCount += MergeImportStubs(importStubs, moduleImage.ImportStubs, modulePath);
|
||||||
|
mergedSymbolCount += MergeRuntimeSymbols(runtimeSymbols, moduleImage.RuntimeSymbols);
|
||||||
|
RegisterLoadedModule(modulePath, moduleImage, isMain: false, isSystemModule: false);
|
||||||
|
loadedModules++;
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[RUNTIME] Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
failedModules++;
|
||||||
|
Console.Error.WriteLine($"[RUNTIME] Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[RUNTIME] Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int MergeImportStubs(
|
||||||
|
IDictionary<ulong, string> destination,
|
||||||
|
IReadOnlyDictionary<ulong, string> source,
|
||||||
|
string modulePath)
|
||||||
|
{
|
||||||
|
var added = 0;
|
||||||
|
foreach (var (address, nid) in source)
|
||||||
|
{
|
||||||
|
if (destination.TryGetValue(address, out var existingNid))
|
||||||
|
{
|
||||||
|
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[RUNTIME] Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
destination[address] = nid;
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int MergeRuntimeSymbols(
|
||||||
|
IDictionary<string, ulong> destination,
|
||||||
|
IReadOnlyDictionary<string, ulong> source)
|
||||||
|
{
|
||||||
|
var added = 0;
|
||||||
|
foreach (var (name, address) in source)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name) || !IsUsableRuntimeSymbolAddress(address))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destination.TryGetValue(name, out var existingAddress))
|
||||||
|
{
|
||||||
|
if (IsPreferredRuntimeSymbolAddress(existingAddress, address))
|
||||||
|
{
|
||||||
|
destination[name] = address;
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
destination[name] = address;
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsPreferredRuntimeSymbolAddress(ulong existingAddress, ulong candidateAddress)
|
||||||
|
{
|
||||||
|
return !IsUsableRuntimeSymbolAddress(existingAddress) && IsUsableRuntimeSymbolAddress(candidateAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsUsableRuntimeSymbolAddress(ulong address)
|
||||||
|
{
|
||||||
|
return address >= 0x10000 && !IsUnresolvedRuntimeSentinel(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsUnresolvedRuntimeSentinel(ulong value)
|
||||||
|
{
|
||||||
|
return value == 0xFFFEUL ||
|
||||||
|
value == 0xFFFFFFFEUL ||
|
||||||
|
value == 0xFFFFFFFFFFFFFFFEUL;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldPreloadModule(string modulePath)
|
||||||
|
{
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_PRELOAD_ALL_SCE_MODULES"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileName = Path.GetFileName(modulePath);
|
||||||
|
if (string.IsNullOrWhiteSpace(fileName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !PreloadSkipModules.Contains(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RegisterLoadedModule(string modulePath, SelfImage image, bool isMain, bool isSystemModule)
|
||||||
|
{
|
||||||
|
if (!TryComputeImageRange(image, out var baseAddress, out var size))
|
||||||
|
{
|
||||||
|
baseAddress = 0;
|
||||||
|
size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var handle = KernelModuleRegistry.RegisterModule(
|
||||||
|
modulePath,
|
||||||
|
baseAddress,
|
||||||
|
size,
|
||||||
|
image.EntryPoint,
|
||||||
|
isMain,
|
||||||
|
isSystemModule);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[RUNTIME] Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryComputeImageRange(SelfImage image, out ulong baseAddress, out ulong size)
|
||||||
|
{
|
||||||
|
baseAddress = 0;
|
||||||
|
size = 0;
|
||||||
|
if (image.ProgramHeaders.Count == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageBase = image.EntryPoint >= image.ElfHeader.EntryPoint
|
||||||
|
? image.EntryPoint - image.ElfHeader.EntryPoint
|
||||||
|
: 0UL;
|
||||||
|
var found = false;
|
||||||
|
ulong minAddress = ulong.MaxValue;
|
||||||
|
ulong maxAddress = 0;
|
||||||
|
for (var i = 0; i < image.ProgramHeaders.Count; i++)
|
||||||
|
{
|
||||||
|
var header = image.ProgramHeaders[i];
|
||||||
|
if (header.HeaderType != ProgramHeaderType.Load || header.MemorySize == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var segmentStart = unchecked(imageBase + header.VirtualAddress);
|
||||||
|
var segmentEnd = unchecked(segmentStart + header.MemorySize);
|
||||||
|
if (!found || segmentStart < minAddress)
|
||||||
|
{
|
||||||
|
minAddress = segmentStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found || segmentEnd > maxAddress)
|
||||||
|
{
|
||||||
|
maxAddress = segmentEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found || maxAddress <= minAddress)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
baseAddress = minAddress;
|
||||||
|
size = maxAddress - minAddress;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildSessionSummary(CpuSessionSummary summary)
|
||||||
|
{
|
||||||
|
var resultText = summary.Result == OrbisGen2Result.ORBIS_GEN2_OK ? "OK" : summary.Result.ToString();
|
||||||
|
var exitText = summary.ExitCode.HasValue ? summary.ExitCode.Value.ToString() : "?";
|
||||||
|
return
|
||||||
|
$"Summary: result={resultText} reason={summary.Reason} exit={exitText} last_guest_rip=0x{summary.LastGuestRip:X16} last_stub_rip=0x{summary.LastStubRip:X16} instr={summary.TotalInstructions} imports={summary.ImportsHit} unique_nids={summary.UniqueNidsHit}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ReadOpcodePreview(ulong instructionPointer, int maxBytes)
|
||||||
|
{
|
||||||
|
if (maxBytes <= 0)
|
||||||
|
{
|
||||||
|
return "??";
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> oneByte = stackalloc byte[1];
|
||||||
|
var parts = new string[maxBytes];
|
||||||
|
var count = 0;
|
||||||
|
for (var i = 0; i < maxBytes; i++)
|
||||||
|
{
|
||||||
|
if (!_virtualMemory.TryRead(instructionPointer + (ulong)i, oneByte))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
parts[count] = oneByte[0].ToString("X2");
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count == 0 ? "??" : string.Join(' ', parts, 0, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryReadUInt64At(ulong address, out ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
|
if (!_virtualMemory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryDecodeInstructionAt(ulong instructionPointer, out DecodedInst decodedInstruction)
|
||||||
|
{
|
||||||
|
if (!IcedDecoder.TryReadGuestBytes(_virtualMemory, instructionPointer, maxLen: 15, out var instructionBytes))
|
||||||
|
{
|
||||||
|
decodedInstruction = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IcedDecoder.TryDecode(instructionPointer, instructionBytes, out decodedInstruction);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsInvalidLongModeOpcode(byte opcode)
|
||||||
|
{
|
||||||
|
return opcode is
|
||||||
|
0x06 or // PUSH ES
|
||||||
|
0x07 or // POP ES
|
||||||
|
0x0E or // PUSH CS
|
||||||
|
0x16 or // PUSH SS
|
||||||
|
0x17 or // POP SS
|
||||||
|
0x1E or // PUSH DS
|
||||||
|
0x1F or // POP DS
|
||||||
|
0x27 or // DAA
|
||||||
|
0x2F or // DAS
|
||||||
|
0x37 or // AAA
|
||||||
|
0x3F or // AAS
|
||||||
|
0x60 or // PUSHA/PUSHAD
|
||||||
|
0x61 or // POPA/POPAD
|
||||||
|
0xD4 or // AAM
|
||||||
|
0xD5; // AAD
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildDecodedInstructionFields(in DecodedInst instruction, string fieldPrefix = "inst")
|
||||||
|
{
|
||||||
|
var text = $", {fieldPrefix}={instruction.Text}, {fieldPrefix}_len={instruction.Length}, {fieldPrefix}_mnemonic={instruction.Mnemonic}, {fieldPrefix}_flow={instruction.FlowControl}";
|
||||||
|
if (instruction.NearBranchTarget is { } target)
|
||||||
|
{
|
||||||
|
text += $", {fieldPrefix}_target=0x{target:X16}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instruction.MemoryAddress is { } memoryAddress)
|
||||||
|
{
|
||||||
|
text += $", {fieldPrefix}_mem=0x{memoryAddress:X16}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instruction.Bytes.Length > 0)
|
||||||
|
{
|
||||||
|
text += $", {fieldPrefix}_bytes={IcedDecoder.FormatBytes(instruction.Bytes)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OrbisGen2Result DispatchHleCall(string nid, CpuContext context)
|
||||||
|
{
|
||||||
|
return _moduleManager.Dispatch(nid, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
|
||||||
|
if (_cpuDispatcher is IDisposable disposableDispatcher)
|
||||||
|
{
|
||||||
|
disposableDispatcher.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_virtualMemory is IDisposable disposableMemory)
|
||||||
|
{
|
||||||
|
disposableMemory.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
15
src/SharpEmu.Core/Runtime/SharpEmuRuntimeOptions.cs
Normal file
15
src/SharpEmu.Core/Runtime/SharpEmuRuntimeOptions.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Runtime;
|
||||||
|
|
||||||
|
using SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
|
public readonly struct SharpEmuRuntimeOptions
|
||||||
|
{
|
||||||
|
public CpuExecutionEngine CpuEngine { get; init; }
|
||||||
|
|
||||||
|
public bool StrictDynlibResolution { get; init; }
|
||||||
|
|
||||||
|
public int ImportTraceLimit { get; init; }
|
||||||
|
}
|
||||||
20
src/SharpEmu.Core/SharpEmu.Core.csproj
Normal file
20
src/SharpEmu.Core/SharpEmu.Core.csproj
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
||||||
|
<ProjectReference Include="..\SharpEmu.Libs\SharpEmu.Libs.csproj" />
|
||||||
|
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Iced" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
25
src/SharpEmu.Core/packages.lock.json
Normal file
25
src/SharpEmu.Core/packages.lock.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"dependencies": {
|
||||||
|
"net10.0": {
|
||||||
|
"Iced": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[1.21.0, )",
|
||||||
|
"resolved": "1.21.0",
|
||||||
|
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
||||||
|
},
|
||||||
|
"sharpemu.hle": {
|
||||||
|
"type": "Project"
|
||||||
|
},
|
||||||
|
"sharpemu.libs": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"SharpEmu.HLE": "[1.0.0, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sharpemu.logging": {
|
||||||
|
"type": "Project"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
156
src/SharpEmu.HLE/Aerolib/Aerolib.cs
Normal file
156
src/SharpEmu.HLE/Aerolib/Aerolib.cs
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
public sealed class Aerolib : ISymbolCatalog
|
||||||
|
{
|
||||||
|
private static readonly Lazy<Aerolib> _instance = new(() => new Aerolib());
|
||||||
|
private static readonly Aerolib EmptyCatalog = new Aerolib(empty: true);
|
||||||
|
|
||||||
|
private Dictionary<string, SysAbiSymbol> _byNid;
|
||||||
|
private Dictionary<string, SysAbiSymbol> _byExportName;
|
||||||
|
|
||||||
|
public static Aerolib Instance => _instance.Value;
|
||||||
|
|
||||||
|
private Aerolib(
|
||||||
|
Dictionary<string, SysAbiSymbol> byNid,
|
||||||
|
Dictionary<string, SysAbiSymbol> byExportName)
|
||||||
|
{
|
||||||
|
_byNid = byNid;
|
||||||
|
_byExportName = byExportName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Aerolib()
|
||||||
|
{
|
||||||
|
_byNid = new Dictionary<string, SysAbiSymbol>(StringComparer.Ordinal);
|
||||||
|
_byExportName = new Dictionary<string, SysAbiSymbol>(StringComparer.Ordinal);
|
||||||
|
LoadFromEmbeddedBinary();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Aerolib(bool empty)
|
||||||
|
{
|
||||||
|
_byNid = new Dictionary<string, SysAbiSymbol>(StringComparer.Ordinal);
|
||||||
|
_byExportName = new Dictionary<string, SysAbiSymbol>(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ISymbolCatalog Empty => EmptyCatalog;
|
||||||
|
|
||||||
|
public string GetName(string nid)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(nid))
|
||||||
|
return nid ?? string.Empty;
|
||||||
|
|
||||||
|
if (_byNid.TryGetValue(nid, out var symbol))
|
||||||
|
return symbol.ExportName;
|
||||||
|
return nid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetName(string nid, out string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(nid))
|
||||||
|
{
|
||||||
|
name = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_byNid.TryGetValue(nid, out var symbol))
|
||||||
|
{
|
||||||
|
name = symbol.ExportName;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
name = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ContainsNid(string nid)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(nid))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return _byNid.ContainsKey(nid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, string> GetAllNidNames()
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, string>(_byNid.Count, StringComparer.Ordinal);
|
||||||
|
foreach (var kvp in _byNid)
|
||||||
|
{
|
||||||
|
result[kvp.Key] = kvp.Value.ExportName;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count => _byNid.Count;
|
||||||
|
|
||||||
|
public bool TryGetByNid(string nid, out SysAbiSymbol symbol)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
|
||||||
|
return _byNid.TryGetValue(nid, out symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetByExportName(string exportName, out SysAbiSymbol symbol)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(exportName);
|
||||||
|
return _byExportName.TryGetValue(exportName, out symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadFromEmbeddedBinary()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var assembly = typeof(Aerolib).Assembly;
|
||||||
|
var resourceName = assembly.GetManifestResourceNames()
|
||||||
|
.FirstOrDefault(n => n.EndsWith("aerolib.bin", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (resourceName == null)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[AEROLIB] Embedded resource 'aerolib.bin' not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||||
|
if (stream == null)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[AEROLIB] Failed to open embedded resource stream");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = new byte[stream.Length];
|
||||||
|
stream.ReadExactly(data);
|
||||||
|
|
||||||
|
int offset = 0;
|
||||||
|
uint count = BinaryPrimitives.ReadUInt32LittleEndian(data.AsSpan(offset, 4));
|
||||||
|
offset += 4;
|
||||||
|
|
||||||
|
_byNid = new Dictionary<string, SysAbiSymbol>((int)count, StringComparer.Ordinal);
|
||||||
|
_byExportName = new Dictionary<string, SysAbiSymbol>((int)count, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
for (uint i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
byte nidLen = data[offset++];
|
||||||
|
string nid = System.Text.Encoding.UTF8.GetString(data, offset, nidLen);
|
||||||
|
offset += nidLen;
|
||||||
|
|
||||||
|
ushort nameLen = BinaryPrimitives.ReadUInt16LittleEndian(data.AsSpan(offset, 2));
|
||||||
|
offset += 2;
|
||||||
|
string name = System.Text.Encoding.UTF8.GetString(data, offset, nameLen);
|
||||||
|
offset += nameLen;
|
||||||
|
|
||||||
|
var symbol = new SysAbiSymbol(nid, name, name, Generation.Gen5);
|
||||||
|
_byNid[nid] = symbol;
|
||||||
|
_byExportName[name] = symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"[AEROLIB] Loaded {_byNid.Count} NID entries from binary resource");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[AEROLIB] Failed to load embedded aerolib.bin: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
BIN
src/SharpEmu.HLE/Aerolib/aerolib.bin
Normal file
BIN
src/SharpEmu.HLE/Aerolib/aerolib.bin
Normal file
Binary file not shown.
172
src/SharpEmu.HLE/CpuContext.cs
Normal file
172
src/SharpEmu.HLE/CpuContext.cs
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
public sealed class CpuContext
|
||||||
|
{
|
||||||
|
private readonly ulong[] _registers = new ulong[16];
|
||||||
|
private readonly ulong[] _xmmRegisters = new ulong[32];
|
||||||
|
private readonly ulong[] _ymmUpperRegisters = new ulong[32];
|
||||||
|
private bool _raxWritten;
|
||||||
|
|
||||||
|
public CpuContext(ICpuMemory memory, Generation generation)
|
||||||
|
{
|
||||||
|
Memory = memory ?? throw new ArgumentNullException(nameof(memory));
|
||||||
|
TargetGeneration = generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICpuMemory Memory { get; }
|
||||||
|
|
||||||
|
public Generation TargetGeneration { get; }
|
||||||
|
|
||||||
|
public ulong Rip { get; set; }
|
||||||
|
|
||||||
|
public ulong Rflags { get; set; }
|
||||||
|
|
||||||
|
public ulong FsBase { get; set; }
|
||||||
|
|
||||||
|
public ulong GsBase { get; set; }
|
||||||
|
|
||||||
|
public ulong this[CpuRegister register]
|
||||||
|
{
|
||||||
|
get => _registers[(int)register];
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_registers[(int)register] = value;
|
||||||
|
if (register == CpuRegister.Rax)
|
||||||
|
{
|
||||||
|
_raxWritten = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearRaxWriteFlag()
|
||||||
|
{
|
||||||
|
_raxWritten = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool WasRaxWritten => _raxWritten;
|
||||||
|
|
||||||
|
public void GetXmmRegister(int registerIndex, out ulong low, out ulong high)
|
||||||
|
{
|
||||||
|
if ((uint)registerIndex >= 16)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(registerIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = registerIndex * 2;
|
||||||
|
low = _xmmRegisters[offset];
|
||||||
|
high = _xmmRegisters[offset + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetXmmRegister(int registerIndex, ulong low, ulong high)
|
||||||
|
{
|
||||||
|
if ((uint)registerIndex >= 16)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(registerIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = registerIndex * 2;
|
||||||
|
_xmmRegisters[offset] = low;
|
||||||
|
_xmmRegisters[offset + 1] = high;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GetYmmUpper(int registerIndex, out ulong low, out ulong high)
|
||||||
|
{
|
||||||
|
if ((uint)registerIndex >= 16)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(registerIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = registerIndex * 2;
|
||||||
|
low = _ymmUpperRegisters[offset];
|
||||||
|
high = _ymmUpperRegisters[offset + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetYmmUpper(int registerIndex, ulong low, ulong high)
|
||||||
|
{
|
||||||
|
if ((uint)registerIndex >= 16)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(registerIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = registerIndex * 2;
|
||||||
|
_ymmUpperRegisters[offset] = low;
|
||||||
|
_ymmUpperRegisters[offset + 1] = high;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearYmmUpper(int registerIndex)
|
||||||
|
{
|
||||||
|
SetYmmUpper(registerIndex, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearAllYmmUpper()
|
||||||
|
{
|
||||||
|
Array.Clear(_ymmUpperRegisters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GetYmmRegister(
|
||||||
|
int registerIndex,
|
||||||
|
out ulong lowLow,
|
||||||
|
out ulong lowHigh,
|
||||||
|
out ulong highLow,
|
||||||
|
out ulong highHigh)
|
||||||
|
{
|
||||||
|
GetXmmRegister(registerIndex, out lowLow, out lowHigh);
|
||||||
|
GetYmmUpper(registerIndex, out highLow, out highHigh);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetYmmRegister(
|
||||||
|
int registerIndex,
|
||||||
|
ulong lowLow,
|
||||||
|
ulong lowHigh,
|
||||||
|
ulong highLow,
|
||||||
|
ulong highHigh)
|
||||||
|
{
|
||||||
|
SetXmmRegister(registerIndex, lowLow, lowHigh);
|
||||||
|
SetYmmUpper(registerIndex, highLow, highHigh);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryReadUInt64(ulong address, out ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
|
if (!Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryWriteUInt64(ulong address, ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||||
|
return Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool PushUInt64(ulong value)
|
||||||
|
{
|
||||||
|
var rsp = this[CpuRegister.Rsp];
|
||||||
|
rsp -= sizeof(ulong);
|
||||||
|
this[CpuRegister.Rsp] = rsp;
|
||||||
|
return TryWriteUInt64(rsp, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool PopUInt64(out ulong value)
|
||||||
|
{
|
||||||
|
var rsp = this[CpuRegister.Rsp];
|
||||||
|
if (!TryReadUInt64(rsp, out value))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this[CpuRegister.Rsp] = rsp + sizeof(ulong);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/SharpEmu.HLE/CpuRegister.cs
Normal file
39
src/SharpEmu.HLE/CpuRegister.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
public enum CpuRegister : int
|
||||||
|
{
|
||||||
|
Rax = 0,
|
||||||
|
|
||||||
|
Rcx = 1,
|
||||||
|
|
||||||
|
Rdx = 2,
|
||||||
|
|
||||||
|
Rbx = 3,
|
||||||
|
|
||||||
|
Rsp = 4,
|
||||||
|
|
||||||
|
Rbp = 5,
|
||||||
|
|
||||||
|
Rsi = 6,
|
||||||
|
|
||||||
|
Rdi = 7,
|
||||||
|
|
||||||
|
R8 = 8,
|
||||||
|
|
||||||
|
R9 = 9,
|
||||||
|
|
||||||
|
R10 = 10,
|
||||||
|
|
||||||
|
R11 = 11,
|
||||||
|
|
||||||
|
R12 = 12,
|
||||||
|
|
||||||
|
R13 = 13,
|
||||||
|
|
||||||
|
R14 = 14,
|
||||||
|
|
||||||
|
R15 = 15,
|
||||||
|
}
|
||||||
31
src/SharpEmu.HLE/ExportedFunction.cs
Normal file
31
src/SharpEmu.HLE/ExportedFunction.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
public sealed class ExportedFunction
|
||||||
|
{
|
||||||
|
public ExportedFunction(string libraryName, string nid, string name, Generation target, SysAbiFunction function)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(libraryName);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||||
|
ArgumentNullException.ThrowIfNull(function);
|
||||||
|
|
||||||
|
LibraryName = libraryName;
|
||||||
|
Nid = nid;
|
||||||
|
Name = name;
|
||||||
|
Target = target;
|
||||||
|
Function = function;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string LibraryName { get; }
|
||||||
|
|
||||||
|
public string Nid { get; }
|
||||||
|
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
public Generation Target { get; }
|
||||||
|
|
||||||
|
public SysAbiFunction Function { get; }
|
||||||
|
}
|
||||||
14
src/SharpEmu.HLE/Generation.cs
Normal file
14
src/SharpEmu.HLE/Generation.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum Generation
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
Gen4 = 1,
|
||||||
|
|
||||||
|
Gen5 = 2,
|
||||||
|
}
|
||||||
11
src/SharpEmu.HLE/ICpuMemory.cs
Normal file
11
src/SharpEmu.HLE/ICpuMemory.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
public interface ICpuMemory
|
||||||
|
{
|
||||||
|
bool TryRead(ulong virtualAddress, Span<byte> destination);
|
||||||
|
|
||||||
|
bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source);
|
||||||
|
}
|
||||||
21
src/SharpEmu.HLE/IModuleManager.cs
Normal file
21
src/SharpEmu.HLE/IModuleManager.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
public interface IModuleManager
|
||||||
|
{
|
||||||
|
int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null);
|
||||||
|
|
||||||
|
void Freeze();
|
||||||
|
|
||||||
|
bool TryGetFunction(string nid, out Delegate function);
|
||||||
|
|
||||||
|
bool TryGetExport(string nid, out ExportedFunction export);
|
||||||
|
|
||||||
|
bool TryGetExportByName(string exportName, out ExportedFunction export);
|
||||||
|
|
||||||
|
OrbisGen2Result Dispatch(string nid, CpuContext context);
|
||||||
|
}
|
||||||
11
src/SharpEmu.HLE/ISymbolCatalog.cs
Normal file
11
src/SharpEmu.HLE/ISymbolCatalog.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
public interface ISymbolCatalog
|
||||||
|
{
|
||||||
|
bool TryGetByNid(string nid, out SysAbiSymbol symbol);
|
||||||
|
|
||||||
|
bool TryGetByExportName(string exportName, out SysAbiSymbol symbol);
|
||||||
|
}
|
||||||
226
src/SharpEmu.HLE/ModuleManager.cs
Normal file
226
src/SharpEmu.HLE/ModuleManager.cs
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
public sealed class ModuleManager : IModuleManager
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<string, Delegate> _dispatchTable = new(StringComparer.Ordinal);
|
||||||
|
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
|
||||||
|
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
|
||||||
|
private readonly object _registrationGate = new();
|
||||||
|
private bool _isFrozen;
|
||||||
|
|
||||||
|
public int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(assembly);
|
||||||
|
|
||||||
|
lock (_registrationGate)
|
||||||
|
{
|
||||||
|
if (_isFrozen)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Module registration is frozen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var registeredCount = 0;
|
||||||
|
var instances = new Dictionary<Type, object>();
|
||||||
|
|
||||||
|
foreach (var type in assembly.GetTypes())
|
||||||
|
{
|
||||||
|
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static))
|
||||||
|
{
|
||||||
|
var exportAttribute = method.GetCustomAttribute<SysAbiExportAttribute>(inherit: false);
|
||||||
|
if (exportAttribute is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var exportInfo = ResolveExportInfo(exportAttribute, method, generation, symbolCatalog);
|
||||||
|
if (exportInfo is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var handler = CreateHandler(type, method, instances);
|
||||||
|
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_exportTable[exportInfo.Value.Nid] = new ExportedFunction(
|
||||||
|
exportInfo.Value.LibraryName,
|
||||||
|
exportInfo.Value.Nid,
|
||||||
|
exportInfo.Value.ExportName,
|
||||||
|
exportInfo.Value.Target,
|
||||||
|
(SysAbiFunction)handler);
|
||||||
|
_exportNameTable.TryAdd(exportInfo.Value.ExportName, _exportTable[exportInfo.Value.Nid]);
|
||||||
|
|
||||||
|
registeredCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return registeredCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Freeze()
|
||||||
|
{
|
||||||
|
lock (_registrationGate)
|
||||||
|
{
|
||||||
|
_isFrozen = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetFunction(string nid, out Delegate function)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
|
||||||
|
return _dispatchTable.TryGetValue(nid, out function!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetExport(string nid, out ExportedFunction export)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
|
||||||
|
return _exportTable.TryGetValue(nid, out export!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetExportByName(string exportName, out ExportedFunction export)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(exportName);
|
||||||
|
return _exportNameTable.TryGetValue(exportName, out export!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OrbisGen2Result Dispatch(string nid, CpuContext context)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
|
||||||
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
|
|
||||||
|
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
|
||||||
|
{
|
||||||
|
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
|
return OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((export.Target & context.TargetGeneration) == 0)
|
||||||
|
{
|
||||||
|
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
|
return OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.ClearRaxWriteFlag();
|
||||||
|
int ret = ((SysAbiFunction)function).Invoke(context);
|
||||||
|
|
||||||
|
if (!context.WasRaxWritten)
|
||||||
|
{
|
||||||
|
context[CpuRegister.Rax] = unchecked((ulong)ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (OrbisGen2Result)ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Delegate CreateHandler(Type ownerType, MethodInfo method, IDictionary<Type, object> instances)
|
||||||
|
{
|
||||||
|
ValidateSignature(method);
|
||||||
|
|
||||||
|
object? target = null;
|
||||||
|
if (!method.IsStatic)
|
||||||
|
{
|
||||||
|
if (!instances.TryGetValue(ownerType, out target))
|
||||||
|
{
|
||||||
|
target = Activator.CreateInstance(ownerType)
|
||||||
|
?? throw new InvalidOperationException($"Cannot instantiate module type: {ownerType.FullName}");
|
||||||
|
instances.Add(ownerType, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var parameterCount = method.GetParameters().Length;
|
||||||
|
if (parameterCount == 0)
|
||||||
|
{
|
||||||
|
var noArg = method.IsStatic
|
||||||
|
? (Func<int>)method.CreateDelegate(typeof(Func<int>))
|
||||||
|
: (Func<int>)method.CreateDelegate(typeof(Func<int>), target!);
|
||||||
|
|
||||||
|
SysAbiFunction adapter = _ => noArg();
|
||||||
|
return adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
return method.IsStatic
|
||||||
|
? method.CreateDelegate(typeof(SysAbiFunction))
|
||||||
|
: method.CreateDelegate(typeof(SysAbiFunction), target!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateSignature(MethodInfo method)
|
||||||
|
{
|
||||||
|
if (method.ReturnType != typeof(int))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Method {method.DeclaringType?.FullName}.{method.Name} must return int.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var parameters = method.GetParameters();
|
||||||
|
if (parameters.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameters.Length == 1 && parameters[0].ParameterType == typeof(CpuContext))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Method {method.DeclaringType?.FullName}.{method.Name} must accept no arguments or one {nameof(CpuContext)} argument.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ExportInfo? ResolveExportInfo(
|
||||||
|
SysAbiExportAttribute exportAttribute,
|
||||||
|
MethodInfo method,
|
||||||
|
Generation generation,
|
||||||
|
ISymbolCatalog? symbolCatalog)
|
||||||
|
{
|
||||||
|
var target = exportAttribute.Target == Generation.None
|
||||||
|
? generation
|
||||||
|
: exportAttribute.Target;
|
||||||
|
if ((target & generation) == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nid = exportAttribute.Nid;
|
||||||
|
var exportName = exportAttribute.ExportName;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(nid) && !string.IsNullOrWhiteSpace(exportName) && symbolCatalog?.TryGetByExportName(exportName, out var byName) == true)
|
||||||
|
{
|
||||||
|
nid = byName.Nid;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(nid) && symbolCatalog?.TryGetByNid(nid, out var byNid) == true)
|
||||||
|
{
|
||||||
|
exportName = string.IsNullOrWhiteSpace(exportName) ? byNid.ExportName : exportName;
|
||||||
|
target = exportAttribute.Target == Generation.None ? byNid.Target : target;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(nid))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Method {method.DeclaringType?.FullName}.{method.Name} must define a NID or match one in symbols catalog.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(exportName))
|
||||||
|
{
|
||||||
|
exportName = method.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((target & generation) == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var libraryName = string.IsNullOrWhiteSpace(exportAttribute.LibraryName) ? "libKernel" : exportAttribute.LibraryName;
|
||||||
|
return new ExportInfo(nid, exportName, libraryName, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct ExportInfo(string Nid, string ExportName, string LibraryName, Generation Target);
|
||||||
|
}
|
||||||
47
src/SharpEmu.HLE/OrbisGen2Result.cs
Normal file
47
src/SharpEmu.HLE/OrbisGen2Result.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents synthetic kernel-style result codes used by the Gen5 runtime.
|
||||||
|
/// Prefixed with ORBIS_GEN2 to distinguish from PS4-oriented ORBIS_* codes
|
||||||
|
/// used by other emulators such as shadPS4.
|
||||||
|
/// </summary>
|
||||||
|
public enum OrbisGen2Result : int
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates successful completion.
|
||||||
|
/// </summary>
|
||||||
|
ORBIS_GEN2_OK = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that the requested export was not found.
|
||||||
|
/// </summary>
|
||||||
|
ORBIS_GEN2_ERROR_NOT_FOUND = unchecked((int)0x80020002),
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that one or more arguments were invalid.
|
||||||
|
/// </summary>
|
||||||
|
ORBIS_GEN2_ERROR_INVALID_ARGUMENT = unchecked((int)0x80020003),
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that an item already exists.
|
||||||
|
/// </summary>
|
||||||
|
ORBIS_GEN2_ERROR_ALREADY_EXISTS = unchecked((int)0x80020004),
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that behavior is recognized but not implemented yet.
|
||||||
|
/// </summary>
|
||||||
|
ORBIS_GEN2_ERROR_NOT_IMPLEMENTED = unchecked((int)0x8002FFFF),
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that memory access failed.
|
||||||
|
/// </summary>
|
||||||
|
ORBIS_GEN2_ERROR_MEMORY_FAULT = unchecked((int)0x80020101),
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that CPU execution trapped on an unsupported instruction.
|
||||||
|
/// </summary>
|
||||||
|
ORBIS_GEN2_ERROR_CPU_TRAP = unchecked((int)0x80020102),
|
||||||
|
}
|
||||||
14
src/SharpEmu.HLE/SharpEmu.HLE.csproj
Normal file
14
src/SharpEmu.HLE/SharpEmu.HLE.csproj
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="Aerolib\aerolib.bin" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
16
src/SharpEmu.HLE/SysAbiExportAttribute.cs
Normal file
16
src/SharpEmu.HLE/SysAbiExportAttribute.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class SysAbiExportAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string LibraryName { get; set; } = "libKernel";
|
||||||
|
|
||||||
|
public string Nid { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string ExportName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public Generation Target { get; set; } = Generation.None;
|
||||||
|
}
|
||||||
6
src/SharpEmu.HLE/SysAbiFunction.cs
Normal file
6
src/SharpEmu.HLE/SysAbiFunction.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
public delegate int SysAbiFunction(CpuContext context);
|
||||||
26
src/SharpEmu.HLE/SysAbiSymbol.cs
Normal file
26
src/SharpEmu.HLE/SysAbiSymbol.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
public readonly struct SysAbiSymbol
|
||||||
|
{
|
||||||
|
public SysAbiSymbol(string nid, string aliasName, string exportName, Generation target)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(exportName);
|
||||||
|
|
||||||
|
Nid = nid;
|
||||||
|
AliasName = aliasName;
|
||||||
|
ExportName = exportName;
|
||||||
|
Target = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Nid { get; }
|
||||||
|
|
||||||
|
public string AliasName { get; }
|
||||||
|
|
||||||
|
public string ExportName { get; }
|
||||||
|
|
||||||
|
public Generation Target { get; }
|
||||||
|
}
|
||||||
6
src/SharpEmu.HLE/packages.lock.json
Normal file
6
src/SharpEmu.HLE/packages.lock.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"dependencies": {
|
||||||
|
"net10.0": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/SharpEmu.Libs/AppContent/AppContentExports.cs
Normal file
20
src/SharpEmu.Libs/AppContent/AppContentExports.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.AppContent;
|
||||||
|
|
||||||
|
public static class AppContentExports
|
||||||
|
{
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "xnd8BJzAxmk",
|
||||||
|
ExportName = "sceAppContentGetAddcontInfoList",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAppContent")]
|
||||||
|
public static int AppContentGetAddcontInfoList(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = ctx;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
205
src/SharpEmu.Libs/CxxAbiExports.cs
Normal file
205
src/SharpEmu.Libs/CxxAbiExports.cs
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.CxxAbi;
|
||||||
|
|
||||||
|
public static class CxaGuardExports
|
||||||
|
{
|
||||||
|
private sealed class GuardState
|
||||||
|
{
|
||||||
|
public int OwnerThreadId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly ConcurrentDictionary<ulong, GuardState> _inProgress = new();
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "3GPpjQdAMTw",
|
||||||
|
ExportName = "__cxa_guard_acquire",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int CxaGuardAcquire(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var guardPtr = ctx[CpuRegister.Rdi];
|
||||||
|
if (guardPtr == 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentThreadId = Environment.CurrentManagedThreadId;
|
||||||
|
var spinner = new SpinWait();
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (!TryReadGuardInitialized(ctx, guardPtr, out var initialized))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogGuardState(ctx, "guard_acquire", guardPtr, initialized);
|
||||||
|
|
||||||
|
if (initialized)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
LogGuardResult("guard_acquire", guardPtr, result: 0, initialized, inProgress: false, ownerThreadId: 0);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newState = new GuardState
|
||||||
|
{
|
||||||
|
OwnerThreadId = currentThreadId,
|
||||||
|
};
|
||||||
|
if (_inProgress.TryAdd(guardPtr, newState))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 1;
|
||||||
|
LogGuardResult("guard_acquire", guardPtr, result: 1, initialized, inProgress: true, ownerThreadId: currentThreadId);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_inProgress.TryGetValue(guardPtr, out var state))
|
||||||
|
{
|
||||||
|
if (state.OwnerThreadId == currentThreadId)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
LogGuardResult("guard_acquire", guardPtr, result: 0, initialized, inProgress: true, ownerThreadId: state.OwnerThreadId);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spinner.SpinOnce();
|
||||||
|
if (spinner.Count % 32 == 0)
|
||||||
|
{
|
||||||
|
Thread.Yield();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "",
|
||||||
|
ExportName = "__cxa_guard_release",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int CxaGuardRelease(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var guardPtr = ctx[CpuRegister.Rdi];
|
||||||
|
if (guardPtr == 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_inProgress.TryGetValue(guardPtr, out var state) &&
|
||||||
|
state.OwnerThreadId != Environment.CurrentManagedThreadId)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
LogGuardResult("guard_release", guardPtr, result: 0, initialized: false, inProgress: true, ownerThreadId: state.OwnerThreadId);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteGuardInitialized(ctx, guardPtr, initialized: true))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
_inProgress.TryRemove(guardPtr, out _);
|
||||||
|
LogGuardState(ctx, "guard_release", guardPtr, initialized: true);
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "",
|
||||||
|
ExportName = "__cxa_guard_abort",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int CxaGuardAbort(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var guardPtr = ctx[CpuRegister.Rdi];
|
||||||
|
if (guardPtr == 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_inProgress.TryGetValue(guardPtr, out var state) &&
|
||||||
|
state.OwnerThreadId != Environment.CurrentManagedThreadId)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
LogGuardResult("guard_abort", guardPtr, result: 0, initialized: false, inProgress: true, ownerThreadId: state.OwnerThreadId);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = TryWriteGuardInitialized(ctx, guardPtr, initialized: false);
|
||||||
|
_inProgress.TryRemove(guardPtr, out _);
|
||||||
|
LogGuardState(ctx, "guard_abort", guardPtr, initialized: false);
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadGuardInitialized(CpuContext ctx, ulong guardPtr, out bool initialized)
|
||||||
|
{
|
||||||
|
initialized = false;
|
||||||
|
|
||||||
|
var aligned = guardPtr & ~7UL;
|
||||||
|
var shift = (int)((guardPtr & 7UL) * 8);
|
||||||
|
|
||||||
|
if (!ctx.TryReadUInt64(aligned, out var word))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var b0 = (byte)((word >> shift) & 0xFF);
|
||||||
|
initialized = (b0 & 0x01) != 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteGuardInitialized(CpuContext ctx, ulong guardPtr, bool initialized)
|
||||||
|
{
|
||||||
|
var aligned = guardPtr & ~7UL;
|
||||||
|
var shift = (int)((guardPtr & 7UL) * 8);
|
||||||
|
var mask = 0xFFUL << shift;
|
||||||
|
|
||||||
|
if (!ctx.TryReadUInt64(aligned, out var word))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var b0 = (byte)((word >> shift) & 0xFF);
|
||||||
|
b0 = initialized ? (byte)(b0 | 0x01) : (byte)(b0 & ~0x01);
|
||||||
|
|
||||||
|
var newWord = (word & ~mask) | ((ulong)b0 << shift);
|
||||||
|
return ctx.TryWriteUInt64(aligned, newWord);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void LogGuardState(CpuContext ctx, string op, ulong guardPtr, bool initialized)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_GUARDS"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var aligned = guardPtr & ~7UL;
|
||||||
|
var readable = ctx.TryReadUInt64(aligned, out var word);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] {op}: guard=0x{guardPtr:X16} aligned=0x{aligned:X16} init={initialized} word={(readable ? $"0x{word:X16}" : "<unreadable>")}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void LogGuardResult(string op, ulong guardPtr, int result, bool initialized, bool inProgress, int ownerThreadId)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_GUARDS"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] {op}: guard=0x{guardPtr:X16} result={result} init={initialized} in_progress={inProgress} owner_thread={ownerThreadId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
373
src/SharpEmu.Libs/Kernel/KernelExports.cs
Normal file
373
src/SharpEmu.Libs/Kernel/KernelExports.cs
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Kernel;
|
||||||
|
|
||||||
|
public static class KernelExports
|
||||||
|
{
|
||||||
|
private static long _nextThreadId = 1;
|
||||||
|
private static int _nextFileDescriptor = 2;
|
||||||
|
private static readonly object _cxaGate = new();
|
||||||
|
private static readonly List<CxaDestructorEntry> _cxaDestructors = new();
|
||||||
|
|
||||||
|
private readonly record struct CxaDestructorEntry(
|
||||||
|
ulong Function,
|
||||||
|
ulong Argument,
|
||||||
|
ulong ModuleHandle);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "WB66evu8bsU",
|
||||||
|
ExportName = "sceKernelGetCompiledSdkVersion",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelGetCompiledSdkVersion(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = ctx;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "uMei1W9uyNo",
|
||||||
|
ExportName = "exit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int Exit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = ctx;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "XKRegsFpEpk",
|
||||||
|
ExportName = "catchReturnFromMain",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int CatchReturnFromMain(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var status = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] catchReturnFromMain(status={status})");
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "bzQExy189ZI",
|
||||||
|
ExportName = "_init_env",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int InitEnv(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "8G2LB+A3rzg",
|
||||||
|
ExportName = "atexit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Atexit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "tsvEmnenz48",
|
||||||
|
ExportName = "__cxa_atexit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int CxaAtexit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var destructorFunction = ctx[CpuRegister.Rdi];
|
||||||
|
var destructorArgument = ctx[CpuRegister.Rsi];
|
||||||
|
var moduleHandle = ctx[CpuRegister.Rdx];
|
||||||
|
if (destructorFunction == 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_cxaGate)
|
||||||
|
{
|
||||||
|
_cxaDestructors.Add(new CxaDestructorEntry(
|
||||||
|
destructorFunction,
|
||||||
|
destructorArgument,
|
||||||
|
moduleHandle));
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "H2e8t5ScQGc",
|
||||||
|
ExportName = "__cxa_finalize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int CxaFinalize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var moduleHandle = ctx[CpuRegister.Rdi];
|
||||||
|
|
||||||
|
lock (_cxaGate)
|
||||||
|
{
|
||||||
|
if (moduleHandle == 0)
|
||||||
|
{
|
||||||
|
_cxaDestructors.Clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (var i = _cxaDestructors.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (_cxaDestructors[i].ModuleHandle == moduleHandle)
|
||||||
|
{
|
||||||
|
_cxaDestructors.RemoveAt(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "kbw4UHHSYy0",
|
||||||
|
ExportName = "__pthread_cxa_finalize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadCxaFinalize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = ctx;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "6Z83sYWFlA8",
|
||||||
|
ExportName = "_exit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int UnderscoreExit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = ctx;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Ac86z8q7T8A",
|
||||||
|
ExportName = "sceKernelExitSblock",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelExitSblock(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = ctx;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "6UgtwV+0zb4",
|
||||||
|
ExportName = "scePthreadCreate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadCreate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var threadIdAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var nextThreadId = unchecked((ulong)Interlocked.Increment(ref _nextThreadId));
|
||||||
|
if (threadIdAddress != 0 && !ctx.TryWriteUInt64(threadIdAddress, nextThreadId))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "OxhIB8LB-PQ",
|
||||||
|
ExportName = "pthread_create",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadCreate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return PthreadCreate(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "onNY9Byn-W8",
|
||||||
|
ExportName = "scePthreadJoin",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadJoin(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var returnValueAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (returnValueAddress != 0 && !ctx.TryWriteUInt64(returnValueAddress, 0))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "h9CcP3J0oVM",
|
||||||
|
ExportName = "pthread_join",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadJoin(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return PthreadJoin(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "wuCroIGjt2g",
|
||||||
|
ExportName = "open",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int Open(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)Interlocked.Increment(ref _nextFileDescriptor));
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "1G3lF1Gg1k8",
|
||||||
|
ExportName = "sceKernelOpen",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelOpen(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)Interlocked.Increment(ref _nextFileDescriptor));
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "hcuQgD53UxM",
|
||||||
|
ExportName = "printf",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Printf(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ulong fmtPtr = ctx[CpuRegister.Rdi];
|
||||||
|
string fmt = ReadCString(ctx, fmtPtr, 4096);
|
||||||
|
|
||||||
|
ulong[] args =
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rsi],
|
||||||
|
ctx[CpuRegister.Rdx],
|
||||||
|
ctx[CpuRegister.Rcx],
|
||||||
|
ctx[CpuRegister.R8],
|
||||||
|
ctx[CpuRegister.R9],
|
||||||
|
};
|
||||||
|
|
||||||
|
int argIndex = 0;
|
||||||
|
var sb = new System.Text.StringBuilder(fmt.Length + 64);
|
||||||
|
|
||||||
|
for (int i = 0; i < fmt.Length; i++)
|
||||||
|
{
|
||||||
|
char ch = fmt[i];
|
||||||
|
if (ch != '%')
|
||||||
|
{
|
||||||
|
sb.Append(ch);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i + 1 < fmt.Length && fmt[i + 1] == '%')
|
||||||
|
{
|
||||||
|
sb.Append('%');
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < fmt.Length && "-+ #0".IndexOf(fmt[j]) >= 0) j++;
|
||||||
|
while (j < fmt.Length && char.IsDigit(fmt[j])) j++;
|
||||||
|
|
||||||
|
if (j < fmt.Length && fmt[j] == '.')
|
||||||
|
{
|
||||||
|
j++;
|
||||||
|
while (j < fmt.Length && char.IsDigit(fmt[j])) j++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (j >= fmt.Length) break;
|
||||||
|
char spec = fmt[j];
|
||||||
|
i = j;
|
||||||
|
|
||||||
|
ulong a = argIndex < args.Length ? args[argIndex++] : 0;
|
||||||
|
|
||||||
|
switch (spec)
|
||||||
|
{
|
||||||
|
case 's':
|
||||||
|
sb.Append(a == 0 ? "(null)" : ReadCString(ctx, a, 4096));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'd':
|
||||||
|
case 'i':
|
||||||
|
sb.Append(unchecked((int)a));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'u':
|
||||||
|
sb.Append(unchecked((uint)a));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'x':
|
||||||
|
sb.Append(unchecked((uint)a).ToString("x"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'p':
|
||||||
|
sb.Append("0x").Append(a.ToString("x16"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
sb.Append('%').Append(spec);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string outStr = sb.ToString();
|
||||||
|
Console.WriteLine($"[DEBUG][PRINF] {outStr}");
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = (ulong)outStr.Length;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "EMutwaQ34Jo",
|
||||||
|
ExportName = "perror",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Perror(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ulong sPtr = ctx[CpuRegister.Rdi];
|
||||||
|
|
||||||
|
string msg;
|
||||||
|
if (sPtr == 0)
|
||||||
|
{
|
||||||
|
msg = "perror(NULL)";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
msg = ReadCString(ctx, sPtr, 2048);
|
||||||
|
msg = $"perror(\"{msg}\")";
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine(msg);
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadCString(CpuContext ctx, ulong address, int maxLen)
|
||||||
|
{
|
||||||
|
Span<byte> buf = stackalloc byte[maxLen];
|
||||||
|
if (!ctx.Memory.TryRead(address, buf))
|
||||||
|
return $"<unreadable 0x{address:X16}>";
|
||||||
|
|
||||||
|
int len = 0;
|
||||||
|
while (len < buf.Length && buf[len] != 0) len++;
|
||||||
|
|
||||||
|
try { return System.Text.Encoding.UTF8.GetString(buf.Slice(0, len)); }
|
||||||
|
catch { return System.Text.Encoding.ASCII.GetString(buf.Slice(0, len)); }
|
||||||
|
}
|
||||||
|
}
|
||||||
2419
src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs
Normal file
2419
src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs
Normal file
File diff suppressed because it is too large
Load Diff
347
src/SharpEmu.Libs/Kernel/KernelModuleRegistry.cs
Normal file
347
src/SharpEmu.Libs/Kernel/KernelModuleRegistry.cs
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Kernel;
|
||||||
|
|
||||||
|
public static class KernelModuleRegistry
|
||||||
|
{
|
||||||
|
private static readonly object _gate = new();
|
||||||
|
private static readonly Dictionary<int, ModuleEntry> _modulesByHandle = new();
|
||||||
|
private static readonly Dictionary<string, int> _handleByPath = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private static readonly Dictionary<string, int> _handleByName = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private static readonly Dictionary<int, int> _sysmoduleHandleById = new();
|
||||||
|
private static int _nextHandle = 1;
|
||||||
|
|
||||||
|
private static readonly Dictionary<int, string[]> KnownSysmoduleNames = new()
|
||||||
|
{
|
||||||
|
[0x0006] = new[] { "libSceFiber.sprx", "libSceFiber.prx" },
|
||||||
|
[0x0021] = new[] { "libSceRudp.sprx", "libSceRudp.prx" },
|
||||||
|
[0x0095] = new[] { "libSceIme.sprx", "libSceIme.prx" },
|
||||||
|
[0x0096] = new[] { "libSceImeDialog.sprx", "libSceImeDialog.prx" },
|
||||||
|
[0x00A9] = new[] { "libSceMouse.sprx", "libSceMouse.prx" },
|
||||||
|
};
|
||||||
|
|
||||||
|
public readonly record struct ModuleEntry(
|
||||||
|
int Handle,
|
||||||
|
string Name,
|
||||||
|
string Path,
|
||||||
|
ulong BaseAddress,
|
||||||
|
ulong EndAddress,
|
||||||
|
ulong EntryPoint,
|
||||||
|
bool IsMain,
|
||||||
|
bool IsSystemModule);
|
||||||
|
|
||||||
|
public static void Reset()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_modulesByHandle.Clear();
|
||||||
|
_handleByPath.Clear();
|
||||||
|
_handleByName.Clear();
|
||||||
|
_sysmoduleHandleById.Clear();
|
||||||
|
_nextHandle = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int RegisterModule(
|
||||||
|
string? modulePath,
|
||||||
|
ulong baseAddress,
|
||||||
|
ulong size,
|
||||||
|
ulong entryPoint,
|
||||||
|
bool isMain,
|
||||||
|
bool isSystemModule = false)
|
||||||
|
{
|
||||||
|
var normalizedPath = NormalizePath(modulePath);
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalizedPath) &&
|
||||||
|
_handleByPath.TryGetValue(normalizedPath, out var existingHandle) &&
|
||||||
|
_modulesByHandle.TryGetValue(existingHandle, out var existing))
|
||||||
|
{
|
||||||
|
var updated = existing with
|
||||||
|
{
|
||||||
|
BaseAddress = baseAddress,
|
||||||
|
EndAddress = ComputeEnd(baseAddress, size),
|
||||||
|
EntryPoint = entryPoint,
|
||||||
|
IsMain = existing.IsMain || isMain,
|
||||||
|
IsSystemModule = existing.IsSystemModule || isSystemModule,
|
||||||
|
};
|
||||||
|
_modulesByHandle[existingHandle] = updated;
|
||||||
|
_handleByName[updated.Name] = existingHandle;
|
||||||
|
return existingHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
var handle = _nextHandle++;
|
||||||
|
var name = ResolveName(normalizedPath, handle);
|
||||||
|
var entry = new ModuleEntry(
|
||||||
|
Handle: handle,
|
||||||
|
Name: name,
|
||||||
|
Path: normalizedPath,
|
||||||
|
BaseAddress: baseAddress,
|
||||||
|
EndAddress: ComputeEnd(baseAddress, size),
|
||||||
|
EntryPoint: entryPoint,
|
||||||
|
IsMain: isMain,
|
||||||
|
IsSystemModule: isSystemModule);
|
||||||
|
_modulesByHandle[handle] = entry;
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalizedPath))
|
||||||
|
{
|
||||||
|
_handleByPath[normalizedPath] = handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleByName[name] = handle;
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int RegisterSyntheticModule(string moduleName, bool isSystemModule)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (TryResolveHandleByNameLocked(moduleName, out var existingHandle))
|
||||||
|
{
|
||||||
|
var existing = _modulesByHandle[existingHandle];
|
||||||
|
_modulesByHandle[existingHandle] = existing with { IsSystemModule = existing.IsSystemModule || isSystemModule };
|
||||||
|
return existingHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
var handle = _nextHandle++;
|
||||||
|
var fallbackName = string.IsNullOrWhiteSpace(moduleName)
|
||||||
|
? $"module_{handle:X4}.sprx"
|
||||||
|
: moduleName;
|
||||||
|
var entry = new ModuleEntry(
|
||||||
|
Handle: handle,
|
||||||
|
Name: fallbackName,
|
||||||
|
Path: string.Empty,
|
||||||
|
BaseAddress: 0,
|
||||||
|
EndAddress: 0,
|
||||||
|
EntryPoint: 0,
|
||||||
|
IsMain: false,
|
||||||
|
IsSystemModule: isSystemModule);
|
||||||
|
_modulesByHandle[handle] = entry;
|
||||||
|
_handleByName[fallbackName] = handle;
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int MarkSysmoduleLoaded(int sysmoduleId)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_sysmoduleHandleById.TryGetValue(sysmoduleId, out var existingHandle) &&
|
||||||
|
_modulesByHandle.ContainsKey(existingHandle))
|
||||||
|
{
|
||||||
|
return existingHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
var handle = TryResolveKnownSysmoduleHandleLocked(sysmoduleId, out var knownHandle)
|
||||||
|
? knownHandle
|
||||||
|
: RegisterSyntheticModule($"sysmodule_0x{sysmoduleId:X4}.sprx", isSystemModule: true);
|
||||||
|
_sysmoduleHandleById[sysmoduleId] = handle;
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MarkSysmoduleUnloaded(int sysmoduleId)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_sysmoduleHandleById.Remove(sysmoduleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsSysmoduleLoaded(int sysmoduleId)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
return _sysmoduleHandleById.ContainsKey(sysmoduleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetModuleByAddress(ulong address, out ModuleEntry module)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
ModuleEntry? best = null;
|
||||||
|
foreach (var entry in _modulesByHandle.Values)
|
||||||
|
{
|
||||||
|
if (entry.BaseAddress == 0 || entry.EndAddress <= entry.BaseAddress)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address < entry.BaseAddress || address >= entry.EndAddress)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (best is null || (entry.EndAddress - entry.BaseAddress) < (best.Value.EndAddress - best.Value.BaseAddress))
|
||||||
|
{
|
||||||
|
best = entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (best is null)
|
||||||
|
{
|
||||||
|
module = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
module = best.Value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetModuleByHandle(int handle, out ModuleEntry module)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
return _modulesByHandle.TryGetValue(handle, out module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryFindByPathOrName(string? modulePathOrName, out ModuleEntry module)
|
||||||
|
{
|
||||||
|
module = default;
|
||||||
|
if (string.IsNullOrWhiteSpace(modulePathOrName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedPath = NormalizePath(modulePathOrName);
|
||||||
|
var fileName = ResolveName(normalizedPath, 0);
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalizedPath) &&
|
||||||
|
_handleByPath.TryGetValue(normalizedPath, out var handleByPath) &&
|
||||||
|
_modulesByHandle.TryGetValue(handleByPath, out module))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryResolveHandleByNameLocked(fileName, out var handleByName) &&
|
||||||
|
_modulesByHandle.TryGetValue(handleByName, out module))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int[] GetModuleHandles(bool includeSystemModules)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
return _modulesByHandle.Values
|
||||||
|
.Where(entry => includeSystemModules || !entry.IsSystemModule)
|
||||||
|
.OrderBy(entry => entry.Handle)
|
||||||
|
.Select(entry => entry.Handle)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetFirstModule(out ModuleEntry module)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_modulesByHandle.Count == 0)
|
||||||
|
{
|
||||||
|
module = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var first = _modulesByHandle.Values.OrderBy(entry => entry.Handle).First();
|
||||||
|
module = first;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryResolveKnownSysmoduleHandleLocked(int sysmoduleId, out int handle)
|
||||||
|
{
|
||||||
|
handle = 0;
|
||||||
|
if (!KnownSysmoduleNames.TryGetValue(sysmoduleId, out var candidates))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var candidate in candidates)
|
||||||
|
{
|
||||||
|
if (TryResolveHandleByNameLocked(candidate, out handle))
|
||||||
|
{
|
||||||
|
var existing = _modulesByHandle[handle];
|
||||||
|
_modulesByHandle[handle] = existing with { IsSystemModule = true };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryResolveHandleByNameLocked(string moduleName, out int handle)
|
||||||
|
{
|
||||||
|
handle = 0;
|
||||||
|
if (string.IsNullOrWhiteSpace(moduleName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_handleByName.TryGetValue(moduleName, out handle))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var altName = moduleName.EndsWith(".sprx", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? moduleName[..^5] + ".prx"
|
||||||
|
: (moduleName.EndsWith(".prx", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? moduleName[..^4] + ".sprx"
|
||||||
|
: string.Empty);
|
||||||
|
return !string.IsNullOrWhiteSpace(altName) && _handleByName.TryGetValue(altName, out handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong ComputeEnd(ulong baseAddress, ulong size)
|
||||||
|
{
|
||||||
|
if (size == 0)
|
||||||
|
{
|
||||||
|
return baseAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
return unchecked(baseAddress + size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizePath(string? modulePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(modulePath))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Path.GetFullPath(modulePath).Replace('\\', '/');
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return modulePath.Replace('\\', '/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveName(string normalizedPath, int handleHint)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalizedPath))
|
||||||
|
{
|
||||||
|
var fileName = Path.GetFileName(normalizedPath);
|
||||||
|
if (!string.IsNullOrWhiteSpace(fileName))
|
||||||
|
{
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return handleHint <= 0
|
||||||
|
? "module.sprx"
|
||||||
|
: $"module_{handleHint:X4}.sprx";
|
||||||
|
}
|
||||||
|
}
|
||||||
815
src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs
Normal file
815
src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs
Normal file
@@ -0,0 +1,815 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Kernel;
|
||||||
|
|
||||||
|
public static class KernelPthreadCompatExports
|
||||||
|
{
|
||||||
|
private const int MutexTypeNormal = 0;
|
||||||
|
private const int MutexTypeRecursive = 1;
|
||||||
|
private const ulong SyntheticMutexHandleBase = 0x00006000_0000_0000;
|
||||||
|
private const ulong SyntheticMutexAttrHandleBase = 0x00006001_0000_0000;
|
||||||
|
|
||||||
|
private static readonly object _stateGate = new();
|
||||||
|
private static readonly Dictionary<ulong, PthreadMutexState> _mutexStates = new();
|
||||||
|
private static readonly Dictionary<ulong, PthreadMutexAttrState> _mutexAttrStates = new();
|
||||||
|
private static readonly Dictionary<ulong, PthreadCondState> _condStates = new();
|
||||||
|
private static readonly HashSet<ulong> _condAttrStates = new();
|
||||||
|
private static long _nextSyntheticThreadId = 1;
|
||||||
|
private static long _nextSyntheticMutexHandleId = 1;
|
||||||
|
private static long _nextSyntheticMutexAttrHandleId = 1;
|
||||||
|
[ThreadStatic]
|
||||||
|
private static ulong _currentThreadId;
|
||||||
|
|
||||||
|
private sealed class PthreadMutexState
|
||||||
|
{
|
||||||
|
public SemaphoreSlim Semaphore { get; } = new(1, 1);
|
||||||
|
public ulong OwnerThreadId { get; set; }
|
||||||
|
public int RecursionCount { get; set; }
|
||||||
|
public int Type { get; set; } = MutexTypeNormal;
|
||||||
|
public int Protocol { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class PthreadCondState
|
||||||
|
{
|
||||||
|
public int PendingSignals { get; set; }
|
||||||
|
public int Waiters { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct PthreadMutexAttrState(int Type, int Protocol);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "aI+OeCz8xrQ",
|
||||||
|
ExportName = "scePthreadSelf",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadSelf(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var currentThreadId = GetCurrentThreadId();
|
||||||
|
ctx[CpuRegister.Rax] = currentThreadId;
|
||||||
|
TracePthreadSelf(ctx, currentThreadId);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "3PtV6p3QNX4",
|
||||||
|
ExportName = "scePthreadEqual",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadEqual(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var left = ctx[CpuRegister.Rdi];
|
||||||
|
var right = ctx[CpuRegister.Rsi];
|
||||||
|
ctx[CpuRegister.Rax] = left == right ? 1UL : 0UL;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "T72hz6ffq08",
|
||||||
|
ExportName = "scePthreadYield",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadYield(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = ctx;
|
||||||
|
Thread.Yield();
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "EI-5-jlq2dE",
|
||||||
|
ExportName = "scePthreadGetthreadid",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadGetthreadid(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var currentThreadId = GetCurrentThreadId();
|
||||||
|
var outAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (outAddress != 0 && !ctx.TryWriteUInt64(outAddress, currentThreadId))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = currentThreadId;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "cmo1RIYva9o",
|
||||||
|
ExportName = "scePthreadMutexInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadMutexInit(CpuContext ctx) => PthreadMutexInitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi]);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "2Of0f+3mhhE",
|
||||||
|
ExportName = "scePthreadMutexDestroy",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadMutexDestroy(CpuContext ctx) => PthreadMutexDestroyCore(ctx, ctx[CpuRegister.Rdi]);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "9UK1vLZQft4",
|
||||||
|
ExportName = "scePthreadMutexLock",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadMutexLock(CpuContext ctx) => PthreadMutexLockCore(ctx, ctx[CpuRegister.Rdi], tryOnly: false);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "upoVrzMHFeE",
|
||||||
|
ExportName = "scePthreadMutexTrylock",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadMutexTrylock(CpuContext ctx) => PthreadMutexLockCore(ctx, ctx[CpuRegister.Rdi], tryOnly: true);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "tn3VlD0hG60",
|
||||||
|
ExportName = "scePthreadMutexUnlock",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadMutexUnlock(CpuContext ctx) => PthreadMutexUnlockCore(ctx, ctx[CpuRegister.Rdi], requireOwner: true);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "ttHNfU+qDBU",
|
||||||
|
ExportName = "pthread_mutex_init",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadMutexInit(CpuContext ctx) => PthreadMutexInitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi]);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "ltCfaGr2JGE",
|
||||||
|
ExportName = "pthread_mutex_destroy",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadMutexDestroy(CpuContext ctx) => PthreadMutexDestroyCore(ctx, ctx[CpuRegister.Rdi]);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "7H0iTOciTLo",
|
||||||
|
ExportName = "pthread_mutex_lock",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadMutexLock(CpuContext ctx) => PthreadMutexLockCore(ctx, ctx[CpuRegister.Rdi], tryOnly: false);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "K-jXhbt2gn4",
|
||||||
|
ExportName = "pthread_mutex_trylock",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadMutexTrylock(CpuContext ctx) => PthreadMutexLockCore(ctx, ctx[CpuRegister.Rdi], tryOnly: true);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "2Z+PpY6CaJg",
|
||||||
|
ExportName = "pthread_mutex_unlock",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadMutexUnlock(CpuContext ctx) => PthreadMutexUnlockCore(ctx, ctx[CpuRegister.Rdi], requireOwner: true);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "F8bUHwAG284",
|
||||||
|
ExportName = "scePthreadMutexattrInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadMutexattrInit(CpuContext ctx) => PthreadMutexattrInitCore(ctx, ctx[CpuRegister.Rdi]);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "smWEktiyyG0",
|
||||||
|
ExportName = "scePthreadMutexattrDestroy",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadMutexattrDestroy(CpuContext ctx) => PthreadMutexattrDestroyCore(ctx, ctx[CpuRegister.Rdi]);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "iMp8QpE+XO4",
|
||||||
|
ExportName = "scePthreadMutexattrSettype",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadMutexattrSettype(CpuContext ctx) => PthreadMutexattrSettypeCore(ctx, ctx[CpuRegister.Rdi], unchecked((int)ctx[CpuRegister.Rsi]));
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "1FGvU0i9saQ",
|
||||||
|
ExportName = "scePthreadMutexattrSetprotocol",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadMutexattrSetprotocol(CpuContext ctx) => PthreadMutexattrSetprotocolCore(ctx, ctx[CpuRegister.Rdi], unchecked((int)ctx[CpuRegister.Rsi]));
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "dQHWEsJtoE4",
|
||||||
|
ExportName = "pthread_mutexattr_init",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadMutexattrInit(CpuContext ctx) => PthreadMutexattrInitCore(ctx, ctx[CpuRegister.Rdi]);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "HF7lK46xzjY",
|
||||||
|
ExportName = "pthread_mutexattr_destroy",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadMutexattrDestroy(CpuContext ctx) => PthreadMutexattrDestroyCore(ctx, ctx[CpuRegister.Rdi]);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "mDmgMOGVUqg",
|
||||||
|
ExportName = "pthread_mutexattr_settype",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadMutexattrSettype(CpuContext ctx) => PthreadMutexattrSettypeCore(ctx, ctx[CpuRegister.Rdi], unchecked((int)ctx[CpuRegister.Rsi]));
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "2Tb92quprl0",
|
||||||
|
ExportName = "scePthreadCondInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadCondInit(CpuContext ctx) => PthreadCondInitCore(ctx[CpuRegister.Rdi]);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "g+PZd2hiacg",
|
||||||
|
ExportName = "scePthreadCondDestroy",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadCondDestroy(CpuContext ctx) => PthreadCondDestroyCore(ctx[CpuRegister.Rdi]);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "WKAXJ4XBPQ4",
|
||||||
|
ExportName = "scePthreadCondWait",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadCondWait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: false);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "BmMjYxmew1w",
|
||||||
|
ExportName = "scePthreadCondTimedwait",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "kDh-NfxgMtE",
|
||||||
|
ExportName = "scePthreadCondSignal",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx[CpuRegister.Rdi], broadcast: false);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "JGgj7Uvrl+A",
|
||||||
|
ExportName = "scePthreadCondBroadcast",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadCondBroadcast(CpuContext ctx) => PthreadCondSignalCore(ctx[CpuRegister.Rdi], broadcast: true);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Op8TBGY5KHg",
|
||||||
|
ExportName = "pthread_cond_wait",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadCondWait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: false);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "mkx2fVhNMsg",
|
||||||
|
ExportName = "pthread_cond_broadcast",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadCondBroadcast(CpuContext ctx) => PthreadCondSignalCore(ctx[CpuRegister.Rdi], broadcast: true);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "m5-2bsNfv7s",
|
||||||
|
ExportName = "scePthreadCondattrInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadCondattrInit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var attrAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (attrAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_condAttrStates.Add(attrAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "waPcxYiR3WA",
|
||||||
|
ExportName = "scePthreadCondattrDestroy",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadCondattrDestroy(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var attrAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (attrAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_condAttrStates.Remove(attrAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
|
||||||
|
{
|
||||||
|
if (mutexAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var attr = ResolveMutexAttrState(ctx, attrAddress);
|
||||||
|
var state = new PthreadMutexState
|
||||||
|
{
|
||||||
|
Type = attr.Type,
|
||||||
|
Protocol = attr.Protocol,
|
||||||
|
};
|
||||||
|
|
||||||
|
var syntheticHandle = AllocateSyntheticHandle(SyntheticMutexHandleBase, ref _nextSyntheticMutexHandleId);
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_mutexStates[mutexAddress] = state;
|
||||||
|
_mutexStates[syntheticHandle] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = ctx.TryWriteUInt64(mutexAddress, syntheticHandle);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int PthreadMutexDestroyCore(CpuContext ctx, ulong mutexAddress)
|
||||||
|
{
|
||||||
|
if (mutexAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolvedAddress = ResolveMutexHandle(ctx, mutexAddress);
|
||||||
|
PthreadMutexState? state;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_mutexStates.TryGetValue(resolvedAddress, out state);
|
||||||
|
_mutexStates.Remove(resolvedAddress);
|
||||||
|
if (resolvedAddress != mutexAddress)
|
||||||
|
{
|
||||||
|
_mutexStates.Remove(mutexAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state is null)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Semaphore.Dispose();
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int PthreadMutexLockCore(CpuContext ctx, ulong mutexAddress, bool tryOnly)
|
||||||
|
{
|
||||||
|
if (mutexAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolvedAddress = ResolveMutexHandle(ctx, mutexAddress);
|
||||||
|
PthreadMutexState state;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (!_mutexStates.TryGetValue(resolvedAddress, out state!))
|
||||||
|
{
|
||||||
|
state = new PthreadMutexState();
|
||||||
|
_mutexStates[resolvedAddress] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolvedAddress != mutexAddress)
|
||||||
|
{
|
||||||
|
_mutexStates[mutexAddress] = state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentThreadId = GetCurrentThreadId();
|
||||||
|
lock (state)
|
||||||
|
{
|
||||||
|
if (state.OwnerThreadId == currentThreadId)
|
||||||
|
{
|
||||||
|
if (state.Type == MutexTypeRecursive)
|
||||||
|
{
|
||||||
|
state.RecursionCount++;
|
||||||
|
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var acquired = true;
|
||||||
|
if (tryOnly)
|
||||||
|
{
|
||||||
|
acquired = state.Semaphore.Wait(0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state.Semaphore.Wait();
|
||||||
|
}
|
||||||
|
if (!acquired)
|
||||||
|
{
|
||||||
|
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (state)
|
||||||
|
{
|
||||||
|
state.OwnerThreadId = currentThreadId;
|
||||||
|
state.RecursionCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int PthreadMutexUnlockCore(CpuContext ctx, ulong mutexAddress, bool requireOwner)
|
||||||
|
{
|
||||||
|
if (mutexAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolvedAddress = ResolveMutexHandle(ctx, mutexAddress);
|
||||||
|
PthreadMutexState? state;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_mutexStates.TryGetValue(resolvedAddress, out state);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state is null)
|
||||||
|
{
|
||||||
|
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, null, GetCurrentThreadId(), (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentThreadId = GetCurrentThreadId();
|
||||||
|
var shouldRelease = false;
|
||||||
|
lock (state)
|
||||||
|
{
|
||||||
|
if (state.RecursionCount <= 0)
|
||||||
|
{
|
||||||
|
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireOwner && state.OwnerThreadId != currentThreadId)
|
||||||
|
{
|
||||||
|
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.RecursionCount--;
|
||||||
|
if (state.RecursionCount == 0)
|
||||||
|
{
|
||||||
|
state.OwnerThreadId = 0;
|
||||||
|
shouldRelease = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRelease)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
state.Semaphore.Release();
|
||||||
|
}
|
||||||
|
catch (SemaphoreFullException)
|
||||||
|
{
|
||||||
|
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int PthreadMutexattrInitCore(CpuContext ctx, ulong attrAddress)
|
||||||
|
{
|
||||||
|
if (attrAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var syntheticHandle = AllocateSyntheticHandle(SyntheticMutexAttrHandleBase, ref _nextSyntheticMutexAttrHandleId);
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_mutexAttrStates[attrAddress] = new PthreadMutexAttrState(MutexTypeNormal, 0);
|
||||||
|
_mutexAttrStates[syntheticHandle] = new PthreadMutexAttrState(MutexTypeNormal, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = ctx.TryWriteUInt64(attrAddress, syntheticHandle);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int PthreadMutexattrDestroyCore(CpuContext ctx, ulong attrAddress)
|
||||||
|
{
|
||||||
|
if (attrAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolvedAddress = ResolveMutexAttrHandle(ctx, attrAddress);
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_mutexAttrStates.Remove(resolvedAddress);
|
||||||
|
if (resolvedAddress != attrAddress)
|
||||||
|
{
|
||||||
|
_mutexAttrStates.Remove(attrAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int PthreadMutexattrSettypeCore(CpuContext ctx, ulong attrAddress, int type)
|
||||||
|
{
|
||||||
|
if (attrAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolvedAddress = ResolveMutexAttrHandle(ctx, attrAddress);
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (!_mutexAttrStates.TryGetValue(resolvedAddress, out var state))
|
||||||
|
{
|
||||||
|
state = new PthreadMutexAttrState(MutexTypeNormal, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
_mutexAttrStates[resolvedAddress] = state with { Type = type };
|
||||||
|
if (resolvedAddress != attrAddress)
|
||||||
|
{
|
||||||
|
_mutexAttrStates[attrAddress] = _mutexAttrStates[resolvedAddress];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int PthreadMutexattrSetprotocolCore(CpuContext ctx, ulong attrAddress, int protocol)
|
||||||
|
{
|
||||||
|
if (attrAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolvedAddress = ResolveMutexAttrHandle(ctx, attrAddress);
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (!_mutexAttrStates.TryGetValue(resolvedAddress, out var state))
|
||||||
|
{
|
||||||
|
state = new PthreadMutexAttrState(MutexTypeNormal, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
_mutexAttrStates[resolvedAddress] = state with { Protocol = protocol };
|
||||||
|
if (resolvedAddress != attrAddress)
|
||||||
|
{
|
||||||
|
_mutexAttrStates[attrAddress] = _mutexAttrStates[resolvedAddress];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong ResolveMutexHandle(CpuContext ctx, ulong mutexAddress)
|
||||||
|
{
|
||||||
|
if (mutexAddress == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (_mutexStates.ContainsKey(mutexAddress))
|
||||||
|
{
|
||||||
|
return mutexAddress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.TryReadUInt64(mutexAddress, out var pointedHandle) && pointedHandle != 0)
|
||||||
|
{
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (_mutexStates.ContainsKey(pointedHandle))
|
||||||
|
{
|
||||||
|
return pointedHandle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mutexAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong ResolveMutexAttrHandle(CpuContext ctx, ulong attrAddress)
|
||||||
|
{
|
||||||
|
if (attrAddress == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (_mutexAttrStates.ContainsKey(attrAddress))
|
||||||
|
{
|
||||||
|
return attrAddress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.TryReadUInt64(attrAddress, out var pointedHandle) && pointedHandle != 0)
|
||||||
|
{
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (_mutexAttrStates.ContainsKey(pointedHandle))
|
||||||
|
{
|
||||||
|
return pointedHandle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PthreadMutexAttrState ResolveMutexAttrState(CpuContext ctx, ulong attrAddress)
|
||||||
|
{
|
||||||
|
if (attrAddress == 0)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolvedAddress = ResolveMutexAttrHandle(ctx, attrAddress);
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
return _mutexAttrStates.TryGetValue(resolvedAddress, out var state)
|
||||||
|
? state
|
||||||
|
: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong AllocateSyntheticHandle(ulong baseAddress, ref long nextId)
|
||||||
|
{
|
||||||
|
var id = unchecked((ulong)Interlocked.Increment(ref nextId));
|
||||||
|
return baseAddress + (id << 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int PthreadCondInitCore(ulong condAddress)
|
||||||
|
{
|
||||||
|
if (condAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_condStates[condAddress] = new PthreadCondState();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int PthreadCondDestroyCore(ulong condAddress)
|
||||||
|
{
|
||||||
|
if (condAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_condStates.Remove(condAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int PthreadCondWaitCore(CpuContext ctx, ulong condAddress, ulong mutexAddress, bool timed)
|
||||||
|
{
|
||||||
|
if (condAddress == 0 || mutexAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (!_condStates.TryGetValue(condAddress, out var state))
|
||||||
|
{
|
||||||
|
state = new PthreadCondState();
|
||||||
|
_condStates[condAddress] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Waiters++;
|
||||||
|
if (state.PendingSignals > 0)
|
||||||
|
{
|
||||||
|
state.PendingSignals--;
|
||||||
|
state.Waiters--;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var unlockResult = PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
|
||||||
|
if (unlockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
return unlockResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timed)
|
||||||
|
{
|
||||||
|
Thread.Sleep(1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Thread.Yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
var lockResult = PthreadMutexLockCore(ctx, mutexAddress, tryOnly: false);
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (_condStates.TryGetValue(condAddress, out var state))
|
||||||
|
{
|
||||||
|
state.Waiters = Math.Max(0, state.Waiters - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lockResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int PthreadCondSignalCore(ulong condAddress, bool broadcast)
|
||||||
|
{
|
||||||
|
if (condAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (!_condStates.TryGetValue(condAddress, out var state))
|
||||||
|
{
|
||||||
|
state = new PthreadCondState();
|
||||||
|
_condStates[condAddress] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (broadcast)
|
||||||
|
{
|
||||||
|
state.PendingSignals += Math.Max(1, state.Waiters);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state.PendingSignals++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong GetCurrentThreadId()
|
||||||
|
{
|
||||||
|
if (_currentThreadId != 0)
|
||||||
|
{
|
||||||
|
return _currentThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentThreadId = unchecked((ulong)Interlocked.Increment(ref _nextSyntheticThreadId));
|
||||||
|
return _currentThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TracePthreadSelf(CpuContext ctx, ulong currentThreadId)
|
||||||
|
{
|
||||||
|
if (!ShouldTracePthread())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] pthread_self: stale_rdi=0x{ctx[CpuRegister.Rdi]:X16} thread=0x{currentThreadId:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TracePthreadMutex(CpuContext ctx, string operation, ulong mutexAddress, ulong resolvedAddress, PthreadMutexState? state, ulong currentThreadId, int result)
|
||||||
|
{
|
||||||
|
if (!ShouldTracePthread())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = ctx.TryReadUInt64(mutexAddress, out var guestWord0);
|
||||||
|
_ = ctx.TryReadUInt64(mutexAddress + 8, out var guestWord1);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] pthread_{operation}: mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} " +
|
||||||
|
$"guest[0]=0x{guestWord0:X16} guest[8]=0x{guestWord1:X16} " +
|
||||||
|
$"current=0x{currentThreadId:X16} owner=0x{(state?.OwnerThreadId ?? 0):X16} " +
|
||||||
|
$"recursion={(state?.RecursionCount ?? 0)} type={(state?.Type ?? 0)} result=0x{unchecked((uint)result):X8}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldTracePthread()
|
||||||
|
{
|
||||||
|
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
}
|
||||||
902
src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs
Normal file
902
src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs
Normal file
@@ -0,0 +1,902 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Kernel;
|
||||||
|
|
||||||
|
public static class KernelPthreadExtendedCompatExports
|
||||||
|
{
|
||||||
|
private const int DefaultThreadPriority = 700;
|
||||||
|
private const ulong DefaultThreadAffinityMask = ulong.MaxValue;
|
||||||
|
private const int DefaultDetachState = 0;
|
||||||
|
private const ulong DefaultGuardSize = 0x1000UL;
|
||||||
|
private const ulong DefaultStackSize = 0x1_00000UL;
|
||||||
|
private const int DefaultInheritSched = 0;
|
||||||
|
private const int DefaultSchedPolicy = 0;
|
||||||
|
private const int DefaultSchedPriority = 0;
|
||||||
|
|
||||||
|
private static readonly object _stateGate = new();
|
||||||
|
private static readonly Dictionary<ulong, ThreadState> _threadStates = new();
|
||||||
|
private static readonly Dictionary<ulong, PthreadAttrState> _attrStates = new();
|
||||||
|
private static readonly Dictionary<ulong, ReaderWriterLockSlim> _rwlockStates = new();
|
||||||
|
private static readonly Dictionary<int, TlsKeyState> _tlsKeys = new();
|
||||||
|
private static int _nextTlsKey = 1;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static Dictionary<int, ulong>? _threadLocalSpecific;
|
||||||
|
|
||||||
|
private sealed class ThreadState
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public int Priority { get; set; } = DefaultThreadPriority;
|
||||||
|
public ulong AffinityMask { get; set; } = DefaultThreadAffinityMask;
|
||||||
|
public int DetachState { get; set; } = DefaultDetachState;
|
||||||
|
public PthreadAttrState Attributes { get; set; } = PthreadAttrState.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct TlsKeyState(ulong Destructor);
|
||||||
|
|
||||||
|
private readonly record struct PthreadAttrState(
|
||||||
|
ulong AffinityMask,
|
||||||
|
int DetachState,
|
||||||
|
ulong StackAddress,
|
||||||
|
ulong StackSize,
|
||||||
|
ulong GuardSize,
|
||||||
|
int InheritSched,
|
||||||
|
int SchedPolicy,
|
||||||
|
int SchedPriority)
|
||||||
|
{
|
||||||
|
public static PthreadAttrState Default =>
|
||||||
|
new(
|
||||||
|
DefaultThreadAffinityMask,
|
||||||
|
DefaultDetachState,
|
||||||
|
0,
|
||||||
|
DefaultStackSize,
|
||||||
|
DefaultGuardSize,
|
||||||
|
DefaultInheritSched,
|
||||||
|
DefaultSchedPolicy,
|
||||||
|
DefaultSchedPriority);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "4qGrR6eoP9Y",
|
||||||
|
ExportName = "scePthreadDetach",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadDetach(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thread = ctx[CpuRegister.Rdi];
|
||||||
|
if (thread == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
var state = GetOrCreateThreadStateLocked(thread);
|
||||||
|
state.DetachState = 1;
|
||||||
|
state.Attributes = state.Attributes with { DetachState = 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "How7B8Oet6k",
|
||||||
|
ExportName = "scePthreadGetname",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadGetname(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thread = ctx[CpuRegister.Rdi];
|
||||||
|
var outNameAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (thread == 0 || outNameAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
string name;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
name = GetOrCreateThreadStateLocked(thread).Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteFixedUtf8CString(ctx, outNameAddress, name, 32))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "bt3CTBKmGyI",
|
||||||
|
ExportName = "scePthreadSetaffinity",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadSetaffinity(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thread = ctx[CpuRegister.Rdi];
|
||||||
|
var mask = ctx[CpuRegister.Rsi];
|
||||||
|
if (thread == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
var state = GetOrCreateThreadStateLocked(thread);
|
||||||
|
state.AffinityMask = mask;
|
||||||
|
state.Attributes = state.Attributes with { AffinityMask = mask };
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "1tKyG7RlMJo",
|
||||||
|
ExportName = "scePthreadGetprio",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadGetprio(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thread = ctx[CpuRegister.Rdi];
|
||||||
|
var outPriorityAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (thread == 0 || outPriorityAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
int priority;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
priority = GetOrCreateThreadStateLocked(thread).Priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteInt32(ctx, outPriorityAddress, priority))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((uint)priority);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "W0Hpm2X0uPE",
|
||||||
|
ExportName = "scePthreadSetprio",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadSetprio(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thread = ctx[CpuRegister.Rdi];
|
||||||
|
var priority = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
if (thread == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
GetOrCreateThreadStateLocked(thread).Priority = priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "nsYoNRywwNg",
|
||||||
|
ExportName = "scePthreadAttrInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadAttrInit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var attrAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (attrAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_attrStates[attrAddress] = PthreadAttrState.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "62KCwEMmzcM",
|
||||||
|
ExportName = "scePthreadAttrDestroy",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadAttrDestroy(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var attrAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (attrAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_attrStates.Remove(attrAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "x1X76arYMxU",
|
||||||
|
ExportName = "scePthreadAttrGet",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadAttrGet(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thread = ctx[CpuRegister.Rdi];
|
||||||
|
var outAttrAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (thread == 0 || outAttrAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
var threadState = GetOrCreateThreadStateLocked(thread);
|
||||||
|
_attrStates[outAttrAddress] = threadState.Attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "8+s5BzZjxSg",
|
||||||
|
ExportName = "scePthreadAttrGetaffinity",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadAttrGetaffinity(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var attrAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var outMaskAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (attrAddress == 0 || outMaskAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
PthreadAttrState state;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
state = GetOrCreateAttrStateLocked(attrAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryWriteUInt64(outMaskAddress, state.AffinityMask))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = state.AffinityMask;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "JaRMy+QcpeU",
|
||||||
|
ExportName = "scePthreadAttrGetdetachstate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadAttrGetdetachstate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var attrAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var outStateAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (attrAddress == 0 || outStateAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
PthreadAttrState state;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
state = GetOrCreateAttrStateLocked(attrAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteInt32(ctx, outStateAddress, state.DetachState))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((uint)state.DetachState);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "txHtngJ+eyc",
|
||||||
|
ExportName = "scePthreadAttrGetguardsize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadAttrGetguardsize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var attrAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var outGuardSizeAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (attrAddress == 0 || outGuardSizeAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
PthreadAttrState state;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
state = GetOrCreateAttrStateLocked(attrAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryWriteUInt64(outGuardSizeAddress, state.GuardSize))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = state.GuardSize;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Ru36fiTtJzA",
|
||||||
|
ExportName = "scePthreadAttrGetstackaddr",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadAttrGetstackaddr(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var attrAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var outStackAddressPointer = ctx[CpuRegister.Rsi];
|
||||||
|
if (attrAddress == 0 || outStackAddressPointer == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
PthreadAttrState state;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
state = GetOrCreateAttrStateLocked(attrAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryWriteUInt64(outStackAddressPointer, state.StackAddress))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = state.StackAddress;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-fA+7ZlGDQs",
|
||||||
|
ExportName = "scePthreadAttrGetstacksize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadAttrGetstacksize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var attrAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var outStackSizeAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (attrAddress == 0 || outStackSizeAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
PthreadAttrState state;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
state = GetOrCreateAttrStateLocked(attrAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryWriteUInt64(outStackSizeAddress, state.StackSize))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = state.StackSize;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "3qxgM4ezETA",
|
||||||
|
ExportName = "scePthreadAttrSetaffinity",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadAttrSetaffinity(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var attrAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var mask = ctx[CpuRegister.Rsi];
|
||||||
|
if (attrAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
var state = GetOrCreateAttrStateLocked(attrAddress);
|
||||||
|
_attrStates[attrAddress] = state with { AffinityMask = mask };
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-Wreprtu0Qs",
|
||||||
|
ExportName = "scePthreadAttrSetdetachstate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadAttrSetdetachstate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var attrAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var detachState = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
if (attrAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
var state = GetOrCreateAttrStateLocked(attrAddress);
|
||||||
|
_attrStates[attrAddress] = state with { DetachState = detachState };
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "El+cQ20DynU",
|
||||||
|
ExportName = "scePthreadAttrSetguardsize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadAttrSetguardsize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var attrAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var guardSize = ctx[CpuRegister.Rsi];
|
||||||
|
if (attrAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
var state = GetOrCreateAttrStateLocked(attrAddress);
|
||||||
|
_attrStates[attrAddress] = state with { GuardSize = guardSize };
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "eXbUSpEaTsA",
|
||||||
|
ExportName = "scePthreadAttrSetinheritsched",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadAttrSetinheritsched(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var attrAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var inheritSched = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
if (attrAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
var state = GetOrCreateAttrStateLocked(attrAddress);
|
||||||
|
_attrStates[attrAddress] = state with { InheritSched = inheritSched };
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "DzES9hQF4f4",
|
||||||
|
ExportName = "scePthreadAttrSetschedparam",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadAttrSetschedparam(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var attrAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var schedParamAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (attrAddress == 0 || schedParamAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
var state = GetOrCreateAttrStateLocked(attrAddress);
|
||||||
|
_attrStates[attrAddress] = state with { SchedPriority = schedPriority };
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "4+h9EzwKF4I",
|
||||||
|
ExportName = "scePthreadAttrSetschedpolicy",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadAttrSetschedpolicy(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var attrAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var policy = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
if (attrAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
var state = GetOrCreateAttrStateLocked(attrAddress);
|
||||||
|
_attrStates[attrAddress] = state with { SchedPolicy = policy };
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "UTXzJbWhhTE",
|
||||||
|
ExportName = "scePthreadAttrSetstacksize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadAttrSetstacksize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var attrAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var stackSize = ctx[CpuRegister.Rsi];
|
||||||
|
if (attrAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
var state = GetOrCreateAttrStateLocked(attrAddress);
|
||||||
|
_attrStates[attrAddress] = state with { StackSize = stackSize };
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "6ULAa0fq4jA",
|
||||||
|
ExportName = "scePthreadRwlockInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadRwlockInit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var rwlockAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (rwlockAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (_rwlockStates.Remove(rwlockAddress, out var existing))
|
||||||
|
{
|
||||||
|
existing.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_rwlockStates[rwlockAddress] = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "BB+kb08Tl9A",
|
||||||
|
ExportName = "scePthreadRwlockDestroy",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadRwlockDestroy(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var rwlockAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (rwlockAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReaderWriterLockSlim? state;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_rwlockStates.Remove(rwlockAddress, out state);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state is null)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Dispose();
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Ox9i0c7L5w0",
|
||||||
|
ExportName = "scePthreadRwlockRdlock",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadRwlockRdlock(CpuContext ctx) => PthreadRwlockLockCore(ctx[CpuRegister.Rdi], write: false);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "mqdNorrB+gI",
|
||||||
|
ExportName = "scePthreadRwlockWrlock",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadRwlockWrlock(CpuContext ctx) => PthreadRwlockLockCore(ctx[CpuRegister.Rdi], write: true);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "+L98PIbGttk",
|
||||||
|
ExportName = "scePthreadRwlockUnlock",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadRwlockUnlock(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var rwlockAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (rwlockAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReaderWriterLockSlim? rwlock;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_rwlockStates.TryGetValue(rwlockAddress, out rwlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rwlock is null)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (rwlock.IsWriteLockHeld)
|
||||||
|
{
|
||||||
|
rwlock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
else if (rwlock.IsReadLockHeld)
|
||||||
|
{
|
||||||
|
rwlock.ExitReadLock();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (SynchronizationLockException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "mqULNdimTn0",
|
||||||
|
ExportName = "pthread_key_create",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadKeyCreate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var outKeyAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var destructor = ctx[CpuRegister.Rsi];
|
||||||
|
if (outKeyAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
int key;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
while (_tlsKeys.ContainsKey(_nextTlsKey))
|
||||||
|
{
|
||||||
|
_nextTlsKey++;
|
||||||
|
}
|
||||||
|
|
||||||
|
key = _nextTlsKey++;
|
||||||
|
_tlsKeys[key] = new TlsKeyState(destructor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteInt32(ctx, outKeyAddress, key))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((uint)key);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "6BpEZuDT7YI",
|
||||||
|
ExportName = "pthread_key_delete",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadKeyDelete(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var key = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (!_tlsKeys.Remove(key))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_threadLocalSpecific?.Remove(key);
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "WrOLvHU0yQM",
|
||||||
|
ExportName = "pthread_setspecific",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadSetspecific(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var key = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var value = ctx[CpuRegister.Rsi];
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (!_tlsKeys.TryGetValue(key, out _))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_threadLocalSpecific ??= new Dictionary<int, ulong>();
|
||||||
|
_threadLocalSpecific[key] = value;
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "0-KXaS70xy4",
|
||||||
|
ExportName = "pthread_getspecific",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadGetspecific(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var key = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (!_tlsKeys.TryGetValue(key, out _))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] =
|
||||||
|
_threadLocalSpecific is not null && _threadLocalSpecific.TryGetValue(key, out var value)
|
||||||
|
? value
|
||||||
|
: 0UL;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int PthreadRwlockLockCore(ulong rwlockAddress, bool write)
|
||||||
|
{
|
||||||
|
if (rwlockAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReaderWriterLockSlim rwlock;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (!_rwlockStates.TryGetValue(rwlockAddress, out rwlock!))
|
||||||
|
{
|
||||||
|
rwlock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
|
||||||
|
_rwlockStates[rwlockAddress] = rwlock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (write)
|
||||||
|
{
|
||||||
|
rwlock.EnterWriteLock();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rwlock.EnterReadLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (LockRecursionException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ThreadState GetOrCreateThreadStateLocked(ulong thread)
|
||||||
|
{
|
||||||
|
if (_threadStates.TryGetValue(thread, out var state))
|
||||||
|
{
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = new ThreadState
|
||||||
|
{
|
||||||
|
Name = $"Thread-{thread:X}",
|
||||||
|
Priority = DefaultThreadPriority,
|
||||||
|
AffinityMask = DefaultThreadAffinityMask,
|
||||||
|
DetachState = DefaultDetachState,
|
||||||
|
Attributes = PthreadAttrState.Default,
|
||||||
|
};
|
||||||
|
_threadStates[thread] = state;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PthreadAttrState GetOrCreateAttrStateLocked(ulong attrAddress)
|
||||||
|
{
|
||||||
|
if (_attrStates.TryGetValue(attrAddress, out var state))
|
||||||
|
{
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = PthreadAttrState.Default;
|
||||||
|
_attrStates[attrAddress] = state;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteFixedUtf8CString(CpuContext ctx, ulong address, string value, int maxBytes)
|
||||||
|
{
|
||||||
|
if (maxBytes <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var utf8 = Encoding.UTF8.GetBytes(value);
|
||||||
|
var payloadLength = Math.Min(utf8.Length, maxBytes - 1);
|
||||||
|
var payload = new byte[payloadLength + 1];
|
||||||
|
utf8.AsSpan(0, payloadLength).CopyTo(payload);
|
||||||
|
payload[^1] = 0;
|
||||||
|
return ctx.Memory.TryWrite(address, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||||
|
if (!ctx.Memory.TryRead(address, bytes))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||||
|
return ctx.Memory.TryWrite(address, bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
1328
src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs
Normal file
1328
src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs
Normal file
File diff suppressed because it is too large
Load Diff
14
src/SharpEmu.Libs/SharpEmu.Libs.csproj
Normal file
14
src/SharpEmu.Libs/SharpEmu.Libs.csproj
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
10
src/SharpEmu.Libs/VideoOut/VideoOutExports.cs
Normal file
10
src/SharpEmu.Libs/VideoOut/VideoOutExports.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.VideoOut;
|
||||||
|
|
||||||
|
public static class VideoOutExports
|
||||||
|
{
|
||||||
|
}
|
||||||
10
src/SharpEmu.Libs/packages.lock.json
Normal file
10
src/SharpEmu.Libs/packages.lock.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"dependencies": {
|
||||||
|
"net10.0": {
|
||||||
|
"sharpemu.hle": {
|
||||||
|
"type": "Project"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
159
src/SharpEmu.Logging/ConsoleLogSink.cs
Normal file
159
src/SharpEmu.Logging/ConsoleLogSink.cs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace SharpEmu.Logging;
|
||||||
|
|
||||||
|
public sealed class ConsoleLogSink : ISharpEmuLogSink
|
||||||
|
{
|
||||||
|
private readonly object _sync = new();
|
||||||
|
|
||||||
|
public ConsoleLogSink(bool useColors = true, bool includeTimestamp = false)
|
||||||
|
{
|
||||||
|
UseColors = useColors;
|
||||||
|
IncludeTimestamp = includeTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool UseColors { get; set; }
|
||||||
|
|
||||||
|
public bool IncludeTimestamp { get; set; }
|
||||||
|
|
||||||
|
public void Write(in LogEntry entry)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
var writer = entry.Level >= LogLevel.Error ? Console.Error : Console.Out;
|
||||||
|
if (IncludeTimestamp)
|
||||||
|
{
|
||||||
|
writer.Write('[');
|
||||||
|
writer.Write(entry.Timestamp.ToString("HH:mm:ss.fff"));
|
||||||
|
writer.Write(']');
|
||||||
|
}
|
||||||
|
|
||||||
|
var levelLabel = ToLevelLabel(entry.Level);
|
||||||
|
WriteLevelSegment(writer, levelLabel, entry.Level);
|
||||||
|
writer.Write('[');
|
||||||
|
writer.Write(entry.Category);
|
||||||
|
writer.Write(']');
|
||||||
|
writer.Write(' ');
|
||||||
|
writer.Write(entry.SourceFileName);
|
||||||
|
if (entry.SourceLine > 0)
|
||||||
|
{
|
||||||
|
writer.Write(':');
|
||||||
|
writer.Write(entry.SourceLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Write(' ');
|
||||||
|
WriteMessageSegment(writer, entry.Message);
|
||||||
|
writer.WriteLine();
|
||||||
|
if (entry.Exception is not null)
|
||||||
|
{
|
||||||
|
writer.WriteLine(entry.Exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ToLevelLabel(LogLevel level)
|
||||||
|
{
|
||||||
|
return level switch
|
||||||
|
{
|
||||||
|
LogLevel.Trace => "TRACE",
|
||||||
|
LogLevel.Debug => "DEBUG",
|
||||||
|
LogLevel.Info => "INFO",
|
||||||
|
LogLevel.Warning => "WARNING",
|
||||||
|
LogLevel.Error => "ERROR",
|
||||||
|
LogLevel.Critical => "CRITICAL",
|
||||||
|
_ => "LOG",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteLevelSegment(TextWriter writer, string label, LogLevel level)
|
||||||
|
{
|
||||||
|
if (!UseColors)
|
||||||
|
{
|
||||||
|
writer.Write('[');
|
||||||
|
writer.Write(label);
|
||||||
|
writer.Write(']');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var originalColor = Console.ForegroundColor;
|
||||||
|
Console.ForegroundColor = GetLevelColor(level);
|
||||||
|
writer.Write('[');
|
||||||
|
writer.Write(label);
|
||||||
|
writer.Write(']');
|
||||||
|
Console.ForegroundColor = originalColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ConsoleColor GetLevelColor(LogLevel level)
|
||||||
|
{
|
||||||
|
return level switch
|
||||||
|
{
|
||||||
|
LogLevel.Trace => ConsoleColor.DarkGray,
|
||||||
|
LogLevel.Debug => ConsoleColor.Gray,
|
||||||
|
LogLevel.Info => ConsoleColor.Blue,
|
||||||
|
LogLevel.Warning => ConsoleColor.Yellow,
|
||||||
|
LogLevel.Error => ConsoleColor.Red,
|
||||||
|
LogLevel.Critical => ConsoleColor.DarkRed,
|
||||||
|
_ => ConsoleColor.White,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteMessageSegment(TextWriter writer, string message)
|
||||||
|
{
|
||||||
|
if (!UseColors || !TryGetMessageColor(message, out var messageColor))
|
||||||
|
{
|
||||||
|
writer.Write(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var originalColor = Console.ForegroundColor;
|
||||||
|
Console.ForegroundColor = messageColor;
|
||||||
|
writer.Write(message);
|
||||||
|
Console.ForegroundColor = originalColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetMessageColor(string message, out ConsoleColor color)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(message))
|
||||||
|
{
|
||||||
|
color = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ContainsIgnoreCase(message, "unresolved import") ||
|
||||||
|
ContainsIgnoreCase(message, "unresolved symbol") ||
|
||||||
|
ContainsIgnoreCase(message, "hot_unresolved_imports=") ||
|
||||||
|
ContainsIgnoreCase(message, "unresolved_imports_hit="))
|
||||||
|
{
|
||||||
|
color = ConsoleColor.Red;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ContainsIgnoreCase(message, "syscall") ||
|
||||||
|
ContainsIgnoreCase(message, "UnhandledSyscall"))
|
||||||
|
{
|
||||||
|
color = ConsoleColor.DarkYellow;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ContainsIgnoreCase(message, "Import trace") ||
|
||||||
|
ContainsIgnoreCase(message, "import_stub_return") ||
|
||||||
|
ContainsIgnoreCase(message, "last_import=") ||
|
||||||
|
ContainsIgnoreCase(message, "hot_imports=") ||
|
||||||
|
ContainsIgnoreCase(message, "resolved import"))
|
||||||
|
{
|
||||||
|
color = ConsoleColor.Blue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
color = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ContainsIgnoreCase(string text, string value)
|
||||||
|
{
|
||||||
|
return text.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/SharpEmu.Logging/ISharpEmuLogSink.cs
Normal file
9
src/SharpEmu.Logging/ISharpEmuLogSink.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Logging;
|
||||||
|
|
||||||
|
public interface ISharpEmuLogSink
|
||||||
|
{
|
||||||
|
void Write(in LogEntry entry);
|
||||||
|
}
|
||||||
14
src/SharpEmu.Logging/LogEntry.cs
Normal file
14
src/SharpEmu.Logging/LogEntry.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Logging;
|
||||||
|
|
||||||
|
public readonly record struct LogEntry(
|
||||||
|
DateTimeOffset Timestamp,
|
||||||
|
LogLevel Level,
|
||||||
|
string Category,
|
||||||
|
string Message,
|
||||||
|
string SourceFileName,
|
||||||
|
int SourceLine,
|
||||||
|
string SourceMemberName,
|
||||||
|
Exception? Exception = null);
|
||||||
21
src/SharpEmu.Logging/LogLevel.cs
Normal file
21
src/SharpEmu.Logging/LogLevel.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Logging;
|
||||||
|
|
||||||
|
public enum LogLevel
|
||||||
|
{
|
||||||
|
Trace = 0,
|
||||||
|
|
||||||
|
Debug = 1,
|
||||||
|
|
||||||
|
Info = 2,
|
||||||
|
|
||||||
|
Warning = 3,
|
||||||
|
|
||||||
|
Error = 4,
|
||||||
|
|
||||||
|
Critical = 5,
|
||||||
|
|
||||||
|
None = 6,
|
||||||
|
}
|
||||||
7
src/SharpEmu.Logging/SharpEmu.Logging.csproj
Normal file
7
src/SharpEmu.Logging/SharpEmu.Logging.csproj
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
</Project>
|
||||||
168
src/SharpEmu.Logging/SharpEmuLog.cs
Normal file
168
src/SharpEmu.Logging/SharpEmuLog.cs
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace SharpEmu.Logging;
|
||||||
|
|
||||||
|
public static class SharpEmuLog
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<string, SharpEmuLogger> LoggerByCategory =
|
||||||
|
new(StringComparer.Ordinal);
|
||||||
|
private static readonly object ConfigurationSync = new();
|
||||||
|
private static volatile LogLevel _minimumLevel = ResolveMinimumLevelFromEnvironment();
|
||||||
|
private static ISharpEmuLogSink _sink = new ConsoleLogSink(
|
||||||
|
useColors: ResolveColorEnabledFromEnvironment(),
|
||||||
|
includeTimestamp: false);
|
||||||
|
|
||||||
|
public static LogLevel MinimumLevel
|
||||||
|
{
|
||||||
|
get => _minimumLevel;
|
||||||
|
set => _minimumLevel = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ISharpEmuLogSink Sink
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (ConfigurationSync)
|
||||||
|
{
|
||||||
|
return _sink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
|
lock (ConfigurationSync)
|
||||||
|
{
|
||||||
|
_sink = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Configure(LogLevel? minimumLevel = null, ISharpEmuLogSink? sink = null)
|
||||||
|
{
|
||||||
|
if (minimumLevel.HasValue)
|
||||||
|
{
|
||||||
|
_minimumLevel = minimumLevel.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sink is not null)
|
||||||
|
{
|
||||||
|
Sink = sink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SharpEmuLogger For(string category)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(category);
|
||||||
|
return LoggerByCategory.GetOrAdd(category, static key => new SharpEmuLogger(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryParseLevel(string? text, out LogLevel level)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
level = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = text.Trim();
|
||||||
|
if (Enum.TryParse<LogLevel>(normalized, ignoreCase: true, out level))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(normalized, "warn", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
level = LogLevel.Warning;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(normalized, "fatal", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
level = LogLevel.Critical;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool IsEnabled(LogLevel level)
|
||||||
|
{
|
||||||
|
var minimum = _minimumLevel;
|
||||||
|
return minimum != LogLevel.None && level >= minimum;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void Write(
|
||||||
|
LogLevel level,
|
||||||
|
string category,
|
||||||
|
string message,
|
||||||
|
Exception? exception,
|
||||||
|
string sourceFilePath,
|
||||||
|
int sourceLine,
|
||||||
|
string sourceMemberName)
|
||||||
|
{
|
||||||
|
if (!IsEnabled(level))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entry = new LogEntry(
|
||||||
|
DateTimeOffset.Now,
|
||||||
|
level,
|
||||||
|
category,
|
||||||
|
message,
|
||||||
|
Path.GetFileName(sourceFilePath),
|
||||||
|
sourceLine,
|
||||||
|
sourceMemberName,
|
||||||
|
exception);
|
||||||
|
|
||||||
|
ISharpEmuLogSink sink;
|
||||||
|
lock (ConfigurationSync)
|
||||||
|
{
|
||||||
|
sink = _sink;
|
||||||
|
}
|
||||||
|
|
||||||
|
sink.Write(in entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LogLevel ResolveMinimumLevelFromEnvironment()
|
||||||
|
{
|
||||||
|
var raw = Environment.GetEnvironmentVariable("SHARPEMU_LOG_LEVEL");
|
||||||
|
return TryParseLevel(raw, out var level) ? level : LogLevel.Info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ResolveColorEnabledFromEnvironment()
|
||||||
|
{
|
||||||
|
if (Console.IsOutputRedirected)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var raw = Environment.GetEnvironmentVariable("SHARPEMU_LOG_NO_COLOR");
|
||||||
|
return !IsTrueLike(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsTrueLike(string? text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.Trim() switch
|
||||||
|
{
|
||||||
|
"1" => true,
|
||||||
|
"true" => true,
|
||||||
|
"TRUE" => true,
|
||||||
|
"yes" => true,
|
||||||
|
"YES" => true,
|
||||||
|
"on" => true,
|
||||||
|
"ON" => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/SharpEmu.Logging/SharpEmuLogger.cs
Normal file
87
src/SharpEmu.Logging/SharpEmuLogger.cs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.Logging;
|
||||||
|
|
||||||
|
public sealed class SharpEmuLogger
|
||||||
|
{
|
||||||
|
public SharpEmuLogger(string category)
|
||||||
|
{
|
||||||
|
Category = category ?? throw new ArgumentNullException(nameof(category));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Category { get; }
|
||||||
|
|
||||||
|
public bool IsEnabled(LogLevel level) => SharpEmuLog.IsEnabled(level);
|
||||||
|
|
||||||
|
public void Trace(
|
||||||
|
string message,
|
||||||
|
[CallerFilePath] string sourceFilePath = "",
|
||||||
|
[CallerLineNumber] int sourceLine = 0,
|
||||||
|
[CallerMemberName] string sourceMemberName = "")
|
||||||
|
=> Write(LogLevel.Trace, message, exception: null, sourceFilePath, sourceLine, sourceMemberName);
|
||||||
|
|
||||||
|
public void Debug(
|
||||||
|
string message,
|
||||||
|
[CallerFilePath] string sourceFilePath = "",
|
||||||
|
[CallerLineNumber] int sourceLine = 0,
|
||||||
|
[CallerMemberName] string sourceMemberName = "")
|
||||||
|
=> Write(LogLevel.Debug, message, exception: null, sourceFilePath, sourceLine, sourceMemberName);
|
||||||
|
|
||||||
|
public void Info(
|
||||||
|
string message,
|
||||||
|
[CallerFilePath] string sourceFilePath = "",
|
||||||
|
[CallerLineNumber] int sourceLine = 0,
|
||||||
|
[CallerMemberName] string sourceMemberName = "")
|
||||||
|
=> Write(LogLevel.Info, message, exception: null, sourceFilePath, sourceLine, sourceMemberName);
|
||||||
|
|
||||||
|
public void Warn(
|
||||||
|
string message,
|
||||||
|
[CallerFilePath] string sourceFilePath = "",
|
||||||
|
[CallerLineNumber] int sourceLine = 0,
|
||||||
|
[CallerMemberName] string sourceMemberName = "")
|
||||||
|
=> Warning(message, sourceFilePath, sourceLine, sourceMemberName);
|
||||||
|
|
||||||
|
public void Warning(
|
||||||
|
string message,
|
||||||
|
[CallerFilePath] string sourceFilePath = "",
|
||||||
|
[CallerLineNumber] int sourceLine = 0,
|
||||||
|
[CallerMemberName] string sourceMemberName = "")
|
||||||
|
=> Write(LogLevel.Warning, message, exception: null, sourceFilePath, sourceLine, sourceMemberName);
|
||||||
|
|
||||||
|
public void Error(
|
||||||
|
string message,
|
||||||
|
Exception? exception = null,
|
||||||
|
[CallerFilePath] string sourceFilePath = "",
|
||||||
|
[CallerLineNumber] int sourceLine = 0,
|
||||||
|
[CallerMemberName] string sourceMemberName = "")
|
||||||
|
=> Write(LogLevel.Error, message, exception, sourceFilePath, sourceLine, sourceMemberName);
|
||||||
|
|
||||||
|
public void Critical(
|
||||||
|
string message,
|
||||||
|
Exception? exception = null,
|
||||||
|
[CallerFilePath] string sourceFilePath = "",
|
||||||
|
[CallerLineNumber] int sourceLine = 0,
|
||||||
|
[CallerMemberName] string sourceMemberName = "")
|
||||||
|
=> Write(LogLevel.Critical, message, exception, sourceFilePath, sourceLine, sourceMemberName);
|
||||||
|
|
||||||
|
private void Write(
|
||||||
|
LogLevel level,
|
||||||
|
string message,
|
||||||
|
Exception? exception,
|
||||||
|
string sourceFilePath,
|
||||||
|
int sourceLine,
|
||||||
|
string sourceMemberName)
|
||||||
|
{
|
||||||
|
SharpEmuLog.Write(
|
||||||
|
level,
|
||||||
|
Category,
|
||||||
|
message,
|
||||||
|
exception,
|
||||||
|
sourceFilePath,
|
||||||
|
sourceLine,
|
||||||
|
sourceMemberName);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/SharpEmu.Logging/packages.lock.json
Normal file
6
src/SharpEmu.Logging/packages.lock.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"dependencies": {
|
||||||
|
"net10.0": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user