Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ cpp_permuter -s Source/PedGroup.cpp -f PedGroup::PromoteMemberToLeader_4C9680 \
| `-c/--compile` | compile command. `{src}` is the candidate source, `{obj}` the object it must write, `{dir}` the candidate's directory, `{root}` the mirror's root (see below) |
| `-t/--target-obj` | object file holding the target code |
| `--symbol`, `--target-symbol` | symbol names, if the guess from `--function` is wrong (MSVC `?Name@Class@@...`, Itanium, or C) |
| `--score-cmd` | score with your own command instead (`{obj}`, `{src}`). It must print the score (0 = match) as the last number in its output |
| `--score-cmd` | score with your own command instead (`{obj}`, `{src}`). It must print the score (0 = match) as the last number in its output. The output is kept as `score_output.txt` with each improvement, so a scorer can print its own asm diff |
| `--op-alias OP=NAME` | for `named_op`: operator `OP` (`*`, `+`, ..., `neg`) also exists as the method `NAME`. Repeatable |
| `--ignore-reloc-names` | compare relocated operands without their symbol names, and treat absolute addresses as symbols. Use this when the target came from raw exe asm |
| `-m exhaustive` | try every candidate the selected passes and combos produce. `--depth N` chains N of them, `--max-candidates` caps the count |
| `-m random` | (default) apply 1..`--max-mutations` random passes or combos, starting from the base or from the best so far. Runs until a match, `-n` compiles, or Ctrl-C |
Expand Down Expand Up @@ -95,6 +96,7 @@ Each improvement goes to `permuter_out/output-<score>-<n>/`:
- `<header>.hpp`: each header that changed, when helpers were permuted.
- `diff.txt`: what changed.
- `asm_diff.txt`: target and candidate asm aligned side by side.
- `score_output.txt`: with `--score-cmd`, what the score command printed.

The exit status is 0 if a match was found and 3 if not.

Expand Down Expand Up @@ -223,6 +225,8 @@ on an example function**, generated from the passes themselves
| `split_case_labels` | `case 1: case 2: S` and `case 1: S case 2: S`, both ways (one jump table entry per case vs a byte index table) |
| `use_getter` | `p->field_1AC_cam.x` and `p->get_cam_x()`, both ways, using the inline getters found in the source and the headers it includes |
| `temp_for_expr` | Compute a cast, a local or `*p` into a new local first (`u8 tmp = (u8)p->a; g(tmp);`). Types come from the declarations; also "copy through a local to get a spill" |
| `named_op` | `a * b` and `a.Multiply_408680(b)`, `-a` and `a.Negate_4086A0()`, both ways, for the operators given with `--op-alias OP=NAME` (`neg` for unary minus). Calling the named out-of-line version decides whether VC6 inlines that use of the operator, which also changes what else it inlines (the inline budget) |
| `cast_operand` | Cast an operand of `/ % >> < > <= >= + - * == !=` to `u32`, `s32`, `u16`, `s16`, `u8` or `s8`, or drop or change an integer cast there: `div` vs `idiv`, `shr` vs `sar`, `jb` vs `jl`, byte adds. Operands that are non-integer locals or calls are left alone |
| `remove_stmt` | Remove an expression statement (low weight) |

decomp-permuter passes left out because they rely on IDO behaviour that VC6 `/O2` folds
Expand Down
62 changes: 62 additions & 0 deletions docs/passes.md
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,68 @@ void f(Ped* p)

(2 candidates in all for this function.)

## `named_op`

'a * b' <-> 'a.Multiply_408680(b)', '-a' <-> 'a.Negate_4086A0()' for the operators given with --op-alias: whether VC6 inlines the operator.

With `--op-alias '*=Multiply_408680' --op-alias neg=Negate_4086A0`:

```cpp
Fix16 f(Fix16 a, Fix16 b)
{
return -a * b;
}
```

becomes

```cpp
Fix16 f(Fix16 a, Fix16 b)
{
return (-a).Multiply_408680(b);
}
```

becomes

```cpp
Fix16 f(Fix16 a, Fix16 b)
{
return a.Negate_4086A0() * b;
}
```

## `cast_operand`

Cast an operand of '/ % >> < > + - * == ...' to an integer type, or drop or change its cast: signed vs unsigned div, shifts, compares and byte maths.

```cpp
u32 f(s32 a, u8 b)
{
return a / b;
}
```

becomes

```cpp
u32 f(s32 a, u8 b)
{
return (u32)a / b;
}
```

becomes

```cpp
u32 f(s32 a, u8 b)
{
return a / (u32)b;
}
```

(12 candidates in all for this function.)

## `remove_stmt`

Remove an expression statement.
Expand Down
12 changes: 12 additions & 0 deletions src/examples.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,14 @@ const std::vector<Sample>& samples() {
{
g((u8)p->a);
})", {0}},
{"named_op", R"(Fix16 f(Fix16 a, Fix16 b)
{
return -a * b;
})", {0, 1}},
{"cast_operand", R"(u32 f(s32 a, u8 b)
{
return a / b;
})", {0, 6}},
{"remove_stmt", R"(void f(Ped* p)
{
g();
Expand Down Expand Up @@ -236,12 +244,16 @@ std::string passExamplesMarkdown() {
"passes also work in the other direction.\n\n";
// use_getter needs a getter to know about
setGetters({{"get_cam_x", "field_1AC_cam.x"}});
// named_op needs to know the named versions of the operators
setOpAliases({{"*", "Multiply_408680"}, {"neg", "Negate_4086A0"}});
for (auto& s : samples()) {
const Pass* p = findPass(s.pass);
if (!p) continue;
o << "## `" << s.pass << "`\n\n" << p->description << ".\n\n";
if (std::string(s.pass) == "use_getter")
o << "With `Fix16 get_cam_x() { return field_1AC_cam.x; }` in an included header:\n\n";
if (std::string(s.pass) == "named_op")
o << "With `--op-alias '*=Multiply_408680' --op-alias neg=Negate_4086A0`:\n\n";
std::string err;
auto f = parseFunc(s.code, err);
std::vector<std::string> outs;
Expand Down
35 changes: 26 additions & 9 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ Finds a source permutation of one function that compiles to the target code.
(default: guessed from --function)
--target-symbol SYM symbol in the target object (default: same as --symbol)
--score-cmd CMD score with this command instead ({obj}, {src}); it must
print the score (lower is better, 0 = match) as its last number
print the score (lower is better, 0 = match) as its last number.
Its output is kept as score_output.txt with each improvement
--objdump PATH llvm-objdump to use (default: llvm-objdump)
--ignore-reloc-names compare relocated operands without their symbol names
(for targets built from raw executable asm)
Expand All @@ -78,6 +79,9 @@ Other functions (for helpers VC6 inlines into the target):
--also [FILE:]NAME also permute this function. FILE is relative to the current
directory or the source's; without it NAME is looked up in
the source and the headers it includes. Repeatable
--op-alias OP=NAME for named_op: operator OP ("*", "+", ..., "neg" for unary
minus) also exists as the method NAME, e.g.
"*=Multiply_408680". Repeatable
--inline-callees also permute the inline functions the target calls: their
definitions in the source (if marked inline) and in the
headers it includes
Expand Down Expand Up @@ -131,7 +135,7 @@ struct Options {
int jobs = 1;
int timeout = 120;
std::string outputDir = "permuter_out";
std::vector<std::string> also, includeDirs, checkParse;
std::vector<std::string> also, includeDirs, checkParse, opAliases;
bool inlineCallees = false, listRegions = false, checkMode = false, showTimings = false, listDefs = false,
passExamples = false;
int maxCallees = 8;
Expand Down Expand Up @@ -211,6 +215,7 @@ bool applyOption(Options& o, const std::string& key, const std::string& val, boo
if (key == "timeout") return num(o.timeout);
if (key == "output-dir" || key == "o") return need() && (o.outputDir = val, true);
if (key == "also") return need() && (o.also.push_back(val), true);
if (key == "op-alias") return need() && (o.opAliases.push_back(val), true);
if (key == "inline-callees") return o.inlineCallees = true;
if (key == "max-callees") return num(o.maxCallees);
if (key == "include-dir" || key == "I") return need() && (o.includeDirs.push_back(val), true);
Expand Down Expand Up @@ -470,10 +475,8 @@ struct Evaluator {
CmdResult r = runCommand(expandTemplate(o.scoreCmd, {{"obj", obj}, {"src", srcPath(w)}}), o.timeout);
bool ok;
double v = lastNumber(r.output, ok);
if (r.status != 0 || !ok) {
log = r.output;
return -1;
}
log = r.output; // on success it goes to score_output.txt (e.g. an asm diff)
if (r.status != 0 || !ok) return -1;
return (long)(v + 0.5);
}
std::vector<Insn> mine;
Expand Down Expand Up @@ -509,7 +512,7 @@ std::string candDiff(const Workspace& ws, const Cand& c) {
}

void writeOutput(const Options& o, const Evaluator& ev, State& st, const Cand& c, long score,
const std::vector<Insn>& insns) {
const std::vector<Insn>& insns, const std::string& scoreOutput) {
const Workspace& ws = ev.ws;
std::string dir = (fs::path(o.outputDir) / ("output-" + std::to_string(score) + "-" +
std::to_string(++st.outputs)))
Expand All @@ -531,6 +534,7 @@ void writeOutput(const Options& o, const Evaluator& ev, State& st, const Cand& c
writeFile(dir + "/diff.txt", diff);
if (!insns.empty()) writeFile(dir + "/asm_diff.txt", diffInsns(ev.sc, ev.target, insns));
writeFile(dir + "/score.txt", std::to_string(score) + "\n");
if (!o.scoreCmd.empty()) writeFile(dir + "/score_output.txt", scoreOutput);
std::cout << "\n[" << score << "] " << (score < st.bestScore ? "new best (was " + std::to_string(st.bestScore) + ")" : std::string("tie"))
<< ", written to " << dir
<< "\n"
Expand All @@ -549,13 +553,13 @@ bool report(const Options& o, const Evaluator& ev, State& st, const Cand& c, lon
}
bool wanted = o.onlyBelow < 0 || score < o.onlyBelow;
if (score < st.bestScore) {
if (wanted) writeOutput(o, ev, st, c, score, insns);
if (wanted) writeOutput(o, ev, st, c, score, insns, log);
st.bestScore = score;
st.best = c;
st.improvements++;
} else if (o.keepTies && score == st.bestScore && score < st.baseScore && wanted && st.ties < 200) {
st.ties++;
writeOutput(o, ev, st, c, score, insns);
writeOutput(o, ev, st, c, score, insns, log);
}
return score == 0 && !o.keepGoing;
}
Expand Down Expand Up @@ -1077,6 +1081,19 @@ int main(int argc, char** argv) {
}
setGetters(gs);
}
// operators with named out-of-line versions, for named_op
{
std::vector<OpAlias> as;
for (auto& spec : o.opAliases) {
size_t eq = spec.find('=');
if (eq == std::string::npos || eq == 0 || eq + 1 == spec.size()) {
std::cerr << "error: --op-alias " << spec << ": expected OP=NAME\n";
return 1;
}
as.push_back({spec.substr(0, eq), spec.substr(eq + 1)});
}
setOpAliases(as);
}
if (o.listRegions || ws.regions.size() > 1) {
std::cout << "permuting:\n";
for (auto& r : ws.regions) std::cout << " " << r.name << " (" << r.file << ")\n";
Expand Down
9 changes: 9 additions & 0 deletions src/passes.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ std::vector<Getter> findGetters(const std::string& text);
void setGetters(std::vector<Getter> g); // before the passes run
const std::vector<Getter>& getters();

// An operator that also exists as a named (out-of-line) method: "*" -> "Multiply_408680",
// "neg" (unary minus) -> "Negate_4086A0". named_op swaps between the two, which decides
// whether VC6 inlines it. Set with --op-alias OP=NAME.
struct OpAlias {
std::string op, method;
};
void setOpAliases(std::vector<OpAlias> a); // before the passes run
const std::vector<OpAlias>& opAliases();

// A pass, or a combination of passes written "a+b+c" on the command line.
struct PassGroup {
std::vector<const Pass*> passes;
Expand Down
115 changes: 115 additions & 0 deletions src/passes_more.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,113 @@ void enumRemoveStmt(const Func& f, const EmitFn& emit) {

} // namespace

// --- named_op ---------------------------------------------------------------------

namespace {
std::vector<OpAlias> gOpAliases;
}

} // namespace util

void setOpAliases(std::vector<OpAlias> a) { util::gOpAliases = std::move(a); }
const std::vector<OpAlias>& opAliases() { return util::gOpAliases; }

namespace util {

// x as the object of a method call: "a.f()" needs parens unless x is postfix-like.
static std::string objectText(const Func& f, const Expr& x) {
std::string s = exprText(f, x);
return isPostfixLike(x) ? s : paren(s);
}

void enumNamedOp(const Func& f, const EmitFn& emit) {
const auto& aliases = opAliases();
if (aliases.empty()) return;
forEachSiteExpr(f, [&](const ExprSite&, std::shared_ptr<Expr>, Expr& x) {
// a * b -> a.Multiply(b), -a -> a.Negate()
if (x.k == Expr::Binary || (x.k == Expr::Unary && f.t(x.op) == "-")) {
std::string op = x.k == Expr::Unary ? "neg" : f.t(x.op);
for (auto& a : aliases) {
if (a.op != op) continue;
std::string text = objectText(f, *x.kids[0]) + "." + a.method + "(" +
(x.k == Expr::Binary ? exprText(f, *x.kids[1]) : "") + ")";
if (!emitReplace(f, emit, x.b, x.e, text)) return false;
}
}
// a.Multiply(b) -> (a * b), a.Negate() -> (-a)
if (x.k == Expr::Call && x.kids[0]->k == Expr::Member) {
const Expr& callee = *x.kids[0];
const Expr& obj = *callee.kids[0];
if (f.t(obj.e) != ".") return true;
const std::string& name = f.t(callee.e - 1);
for (auto& a : aliases) {
if (a.method != name) continue;
std::string text;
if (a.op == "neg" && x.kids.size() == 1)
text = "(-" + operandText(f, obj, 99) + ")";
else if (a.op != "neg" && x.kids.size() == 2)
text = "(" + operandText(f, obj, binPrec(a.op) - 1) + " " + a.op + " " +
operandText(f, *x.kids[1], binPrec(a.op)) + ")";
if (!text.empty() && !emitReplace(f, emit, x.b, x.e, text)) return false;
}
}
return true;
});
}

// --- cast_operand ------------------------------------------------------------------

static const char* const kCastTypes[] = {"u32", "s32", "u16", "s16", "u8", "s8"};

static bool isIntegerType(std::string t) {
t = norm(t);
static const std::set<std::string> ints = {
"u32", "s32", "u16", "s16", "u8", "s8", "int", "unsignedint", "unsigned", "short", "unsignedshort",
"char", "unsignedchar", "signedchar", "long", "unsignedlong", "char_type", "DWORD", "WORD", "BYTE",
"BOOL", "bool", "size_t"};
return ints.count(t) != 0;
}

void enumCastOperand(const Func& f, const EmitFn& emit) {
auto types = localTypes(f);
forEachSiteExpr(f, [&](const ExprSite&, std::shared_ptr<Expr>, Expr& x) {
if (x.k != Expr::Binary) return true;
const std::string& op = f.t(x.op);
static const std::set<std::string> ops = {"/", "%", ">>", "<", ">", "<=", ">=", "+", "-", "*", "==", "!="};
if (!ops.count(op)) return true;
for (int side = 0; side < 2; ++side) {
const Expr& o = *x.kids[side];
if (o.k == Expr::Primary && f.toks[o.b].kind == TokKind::Number) continue;
// Only integers: a local of another type (Fix16, a pointer) or a call result
// would just fail to compile.
if (o.k == Expr::Call) continue;
if (o.k == Expr::Primary && f.isLocal(o.b)) {
auto it = types.find(f.t(o.b));
if (it != types.end() && !isIntegerType(it->second)) continue;
}
const Expr* inner = &o;
std::string have;
if (o.k == Expr::Cast && f.t(o.b) == "(") {
// an existing integer cast: drop it or change its type
int close = matchBracket(f.toks, o.b);
have = norm(f.tokText(o.b + 1, close));
bool known = false;
for (auto* t : kCastTypes) known |= have == t;
if (!known) continue;
inner = o.kids[0].get();
if (!emitReplace(f, emit, o.b, o.e, operandText(f, *inner, binPrec(op))))
return false;
}
for (auto* t : kCastTypes) {
if (have == t) continue;
std::string text = "(" + std::string(t) + ")" + objectText(f, *inner);
if (!emitReplace(f, emit, o.b, o.e, text)) return false;
}
}
return true;
});
}

void registerMorePasses(std::vector<Pass>& passes) {
passes.push_back({"inequalities", "'x > 4' <-> 'x >= 5', 'x < 4' <-> 'x <= 3' (integer constants)", 6,
enumInequalities, nullptr});
Expand Down Expand Up @@ -625,6 +732,14 @@ void registerMorePasses(std::vector<Pass>& passes) {
"Compute a cast, a local or '*p' into a new local first (types from the "
"declarations; also 'copy through a local to get a spill')",
8, enumTempForExpr, nullptr});
passes.push_back({"named_op",
"'a * b' <-> 'a.Multiply_408680(b)', '-a' <-> 'a.Negate_4086A0()' for the "
"operators given with --op-alias: whether VC6 inlines the operator",
8, enumNamedOp, nullptr});
passes.push_back({"cast_operand",
"Cast an operand of '/ % >> < > + - * == ...' to an integer type, or drop or "
"change its cast: signed vs unsigned div, shifts, compares and byte maths",
6, enumCastOperand, nullptr});
passes.push_back({"remove_stmt", "Remove an expression statement", 2, enumRemoveStmt, nullptr});
}

Expand Down
Loading
Loading