jsilnet 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +79 -3
- package/REFERENCE.md +187 -0
- package/example/README.md +22 -0
- package/example/Sample.cs +10 -0
- package/example/Sample.csproj +7 -0
- package/example/edit-constant.js +48 -0
- package/example/inspect.js +37 -0
- package/package.json +42 -6
- package/src/binary/errors.js +17 -0
- package/src/binary/reader.js +206 -0
- package/src/il/body.js +165 -0
- package/src/il/instructions.js +217 -0
- package/src/il/opcodes.js +102 -0
- package/src/il/rewrite.js +331 -0
- package/src/index.js +14774 -0
- package/src/metadata/classic-pdb.js +2530 -0
- package/src/metadata/custom-attributes.js +387 -0
- package/src/metadata/portable-pdb.js +3347 -0
- package/src/metadata/resources.js +37 -0
- package/src/metadata/root-writer.js +54 -0
- package/src/metadata/signatures.js +339 -0
- package/src/metadata/streams.js +285 -0
- package/src/metadata/strong-name.js +229 -0
- package/src/metadata/tables.js +390 -0
- package/src/metadata/writer.js +191 -0
- package/src/pe/headers.js +491 -0
- package/src/resolution/module.js +64 -0
- package/src/writer/patch.js +42 -0
- package/src/writer/pe.js +196 -0
- package/src/writer/structural.js +241 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { FormatError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
export class BinaryReader {
|
|
4
|
+
constructor(input, { start = 0, length, baseOffset = 0, context } = {}) {
|
|
5
|
+
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
6
|
+
const available = bytes.byteLength - start;
|
|
7
|
+
const size = length ?? available;
|
|
8
|
+
if (!Number.isInteger(start) || !Number.isInteger(size) || start < 0 || size < 0 || size > available) {
|
|
9
|
+
throw new RangeError("Invalid binary reader range");
|
|
10
|
+
}
|
|
11
|
+
this.bytes = bytes.subarray(start, start + size);
|
|
12
|
+
this.view = new DataView(this.bytes.buffer, this.bytes.byteOffset, this.bytes.byteLength);
|
|
13
|
+
this.baseOffset = baseOffset;
|
|
14
|
+
this.context = context;
|
|
15
|
+
this.position = 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get remaining() {
|
|
19
|
+
return this.bytes.length - this.position;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get absoluteOffset() {
|
|
23
|
+
return this.baseOffset + this.position;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
ensure(size) {
|
|
27
|
+
if (!Number.isInteger(size) || size < 0 || size > this.remaining) {
|
|
28
|
+
throw new FormatError(`Read of ${size} byte(s) exceeds the available ${this.remaining}`, {
|
|
29
|
+
offset: this.absoluteOffset,
|
|
30
|
+
context: this.context
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
seek(position) {
|
|
36
|
+
if (!Number.isInteger(position) || position < 0 || position > this.bytes.length) {
|
|
37
|
+
throw new FormatError(`Invalid seek position ${position}`, {
|
|
38
|
+
offset: this.baseOffset + Math.max(0, position || 0),
|
|
39
|
+
context: this.context
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
this.position = position;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
skip(size) {
|
|
46
|
+
this.ensure(size);
|
|
47
|
+
this.position += size;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
align(alignment) {
|
|
51
|
+
if (!Number.isInteger(alignment) || alignment < 1 || (alignment & (alignment - 1)) !== 0) {
|
|
52
|
+
throw new RangeError("Alignment must be a positive power of two");
|
|
53
|
+
}
|
|
54
|
+
this.skip((-this.position) & (alignment - 1));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
u1() {
|
|
58
|
+
this.ensure(1);
|
|
59
|
+
return this.view.getUint8(this.position++);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
i1() {
|
|
63
|
+
this.ensure(1);
|
|
64
|
+
return this.view.getInt8(this.position++);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
u2() {
|
|
68
|
+
this.ensure(2);
|
|
69
|
+
const value = this.view.getUint16(this.position, true);
|
|
70
|
+
this.position += 2;
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
i2() {
|
|
75
|
+
this.ensure(2);
|
|
76
|
+
const value = this.view.getInt16(this.position, true);
|
|
77
|
+
this.position += 2;
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
u4() {
|
|
82
|
+
this.ensure(4);
|
|
83
|
+
const value = this.view.getUint32(this.position, true);
|
|
84
|
+
this.position += 4;
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
i4() {
|
|
89
|
+
this.ensure(4);
|
|
90
|
+
const value = this.view.getInt32(this.position, true);
|
|
91
|
+
this.position += 4;
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
u8() {
|
|
96
|
+
this.ensure(8);
|
|
97
|
+
const value = this.view.getBigUint64(this.position, true);
|
|
98
|
+
this.position += 8;
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
f4() {
|
|
103
|
+
this.ensure(4);
|
|
104
|
+
const value = this.view.getFloat32(this.position, true);
|
|
105
|
+
this.position += 4;
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
f8() {
|
|
110
|
+
this.ensure(8);
|
|
111
|
+
const value = this.view.getFloat64(this.position, true);
|
|
112
|
+
this.position += 8;
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
take(size) {
|
|
117
|
+
this.ensure(size);
|
|
118
|
+
const result = this.bytes.subarray(this.position, this.position + size);
|
|
119
|
+
this.position += size;
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
slice(size, context = this.context) {
|
|
124
|
+
const start = this.position;
|
|
125
|
+
this.ensure(size);
|
|
126
|
+
this.position += size;
|
|
127
|
+
return new BinaryReader(this.bytes, {
|
|
128
|
+
start,
|
|
129
|
+
length: size,
|
|
130
|
+
baseOffset: this.baseOffset + start,
|
|
131
|
+
context
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function readCompressedUInt(reader) {
|
|
137
|
+
const first = reader.u1();
|
|
138
|
+
if ((first & 0x80) === 0) return first;
|
|
139
|
+
const offset = reader.absoluteOffset - 1;
|
|
140
|
+
let value;
|
|
141
|
+
if ((first & 0xc0) === 0x80) {
|
|
142
|
+
value = ((first & 0x3f) << 8) | reader.u1();
|
|
143
|
+
if (value < 0x80) {
|
|
144
|
+
throw new FormatError("Non-canonical compressed unsigned integer", {
|
|
145
|
+
offset,
|
|
146
|
+
context: reader.context
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
if ((first & 0xe0) === 0xc0) {
|
|
152
|
+
value = ((first & 0x1f) * 0x1000000) +
|
|
153
|
+
(reader.u1() << 16) +
|
|
154
|
+
(reader.u1() << 8) +
|
|
155
|
+
reader.u1();
|
|
156
|
+
if (value < 0x4000) {
|
|
157
|
+
throw new FormatError("Non-canonical compressed unsigned integer", {
|
|
158
|
+
offset,
|
|
159
|
+
context: reader.context
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
throw new FormatError("Invalid compressed unsigned integer prefix", {
|
|
165
|
+
offset,
|
|
166
|
+
context: reader.context
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function readCompressedInt(reader) {
|
|
171
|
+
const first = reader.u1();
|
|
172
|
+
let encoded;
|
|
173
|
+
let signOffset;
|
|
174
|
+
let width;
|
|
175
|
+
if ((first & 0x80) === 0) {
|
|
176
|
+
encoded = first;
|
|
177
|
+
signOffset = 0x40;
|
|
178
|
+
width = 1;
|
|
179
|
+
} else if ((first & 0xc0) === 0x80) {
|
|
180
|
+
encoded = ((first & 0x3f) << 8) | reader.u1();
|
|
181
|
+
signOffset = 0x2000;
|
|
182
|
+
width = 2;
|
|
183
|
+
} else if ((first & 0xe0) === 0xc0) {
|
|
184
|
+
encoded = (first & 0x1f) * 0x1000000 +
|
|
185
|
+
reader.u1() * 0x10000 +
|
|
186
|
+
reader.u1() * 0x100 +
|
|
187
|
+
reader.u1();
|
|
188
|
+
signOffset = 0x10000000;
|
|
189
|
+
width = 4;
|
|
190
|
+
} else {
|
|
191
|
+
throw new FormatError("Invalid compressed signed integer prefix", {
|
|
192
|
+
offset: reader.absoluteOffset - 1,
|
|
193
|
+
context: reader.context
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const value = Math.floor(encoded / 2);
|
|
197
|
+
const signedValue = (encoded & 1) === 0 ? value : value - signOffset;
|
|
198
|
+
if ((width === 2 && signedValue >= -64 && signedValue <= 63) ||
|
|
199
|
+
(width === 4 && signedValue >= -0x2000 && signedValue <= 0x1fff)) {
|
|
200
|
+
throw new FormatError("Non-canonical compressed signed integer", {
|
|
201
|
+
offset: reader.absoluteOffset - width,
|
|
202
|
+
context: reader.context
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return signedValue;
|
|
206
|
+
}
|
package/src/il/body.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { BinaryReader } from "../binary/reader.js";
|
|
2
|
+
import { FormatError, UnsupportedFormatError } from "../binary/errors.js";
|
|
3
|
+
import { decodeInstructions } from "./instructions.js";
|
|
4
|
+
|
|
5
|
+
function parseExceptionSection(bytes, fileOffset, sectionRva) {
|
|
6
|
+
const reader = new BinaryReader(bytes, {
|
|
7
|
+
start: fileOffset,
|
|
8
|
+
baseOffset: fileOffset,
|
|
9
|
+
context: "method data section"
|
|
10
|
+
});
|
|
11
|
+
const kind = reader.u1();
|
|
12
|
+
const fat = (kind & 0x40) !== 0;
|
|
13
|
+
const moreSections = (kind & 0x80) !== 0;
|
|
14
|
+
if ((kind & 0x3f) !== 0x01) {
|
|
15
|
+
throw new UnsupportedFormatError(`Unsupported method data section kind 0x${kind.toString(16)}`, {
|
|
16
|
+
offset: fileOffset,
|
|
17
|
+
context: "method body"
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
let dataSize;
|
|
21
|
+
if (fat) {
|
|
22
|
+
dataSize = reader.u1() | (reader.u1() << 8) | (reader.u1() << 16);
|
|
23
|
+
} else {
|
|
24
|
+
dataSize = reader.u1();
|
|
25
|
+
reader.u2();
|
|
26
|
+
}
|
|
27
|
+
const clauseSize = fat ? 24 : 12;
|
|
28
|
+
if (dataSize < 4 || (dataSize - 4) % clauseSize !== 0 || dataSize > bytes.length - fileOffset) {
|
|
29
|
+
throw new FormatError("Invalid exception-handler section size", { offset: fileOffset, context: "method body" });
|
|
30
|
+
}
|
|
31
|
+
const clauses = [];
|
|
32
|
+
const clauseReader = new BinaryReader(bytes, {
|
|
33
|
+
start: fileOffset + 4,
|
|
34
|
+
length: dataSize - 4,
|
|
35
|
+
baseOffset: fileOffset + 4,
|
|
36
|
+
context: "exception clauses"
|
|
37
|
+
});
|
|
38
|
+
while (clauseReader.remaining > 0) {
|
|
39
|
+
if (fat) {
|
|
40
|
+
const flags = clauseReader.u4();
|
|
41
|
+
const tryOffset = clauseReader.u4();
|
|
42
|
+
const tryLength = clauseReader.u4();
|
|
43
|
+
const handlerOffset = clauseReader.u4();
|
|
44
|
+
const handlerLength = clauseReader.u4();
|
|
45
|
+
const classTokenOrFilterOffset = clauseReader.u4();
|
|
46
|
+
clauses.push({ flags, tryOffset, tryLength, handlerOffset, handlerLength, classTokenOrFilterOffset });
|
|
47
|
+
} else {
|
|
48
|
+
const flags = clauseReader.u2();
|
|
49
|
+
const tryOffset = clauseReader.u2();
|
|
50
|
+
const tryLength = clauseReader.u1();
|
|
51
|
+
const handlerOffset = clauseReader.u2();
|
|
52
|
+
const handlerLength = clauseReader.u1();
|
|
53
|
+
const classTokenOrFilterOffset = clauseReader.u4();
|
|
54
|
+
clauses.push({ flags, tryOffset, tryLength, handlerOffset, handlerLength, classTokenOrFilterOffset });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
offset: fileOffset,
|
|
59
|
+
rva: sectionRva,
|
|
60
|
+
size: dataSize,
|
|
61
|
+
fat,
|
|
62
|
+
moreSections,
|
|
63
|
+
clauses
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function parseMethodBody(input, fileOffset, methodRva = fileOffset) {
|
|
68
|
+
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
69
|
+
if (fileOffset < 0 || fileOffset >= bytes.length) {
|
|
70
|
+
throw new FormatError("Method body offset is outside the file", { offset: fileOffset, context: "method body" });
|
|
71
|
+
}
|
|
72
|
+
const first = bytes[fileOffset];
|
|
73
|
+
let headerSize;
|
|
74
|
+
let codeSize;
|
|
75
|
+
let maxStack;
|
|
76
|
+
let localVarSigToken;
|
|
77
|
+
let flags;
|
|
78
|
+
let format;
|
|
79
|
+
if ((first & 0x03) === 0x02) {
|
|
80
|
+
format = "tiny";
|
|
81
|
+
headerSize = 1;
|
|
82
|
+
codeSize = first >>> 2;
|
|
83
|
+
maxStack = 8;
|
|
84
|
+
localVarSigToken = 0;
|
|
85
|
+
flags = 0x02;
|
|
86
|
+
} else if ((first & 0x03) === 0x03) {
|
|
87
|
+
format = "fat";
|
|
88
|
+
const header = new BinaryReader(bytes, { start: fileOffset, baseOffset: fileOffset, context: "fat method header" });
|
|
89
|
+
const flagsAndSize = header.u2();
|
|
90
|
+
const headerDwords = flagsAndSize >>> 12;
|
|
91
|
+
headerSize = headerDwords * 4;
|
|
92
|
+
if (headerSize < 12 || fileOffset + headerSize > bytes.length) {
|
|
93
|
+
throw new FormatError(`Invalid fat method header size ${headerSize}`, { offset: fileOffset, context: "method body" });
|
|
94
|
+
}
|
|
95
|
+
flags = flagsAndSize & 0x0fff;
|
|
96
|
+
maxStack = header.u2();
|
|
97
|
+
codeSize = header.u4();
|
|
98
|
+
localVarSigToken = header.u4();
|
|
99
|
+
} else {
|
|
100
|
+
throw new FormatError(`Invalid method header format 0x${(first & 0x03).toString(16)}`, {
|
|
101
|
+
offset: fileOffset,
|
|
102
|
+
context: "method body"
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const codeOffset = fileOffset + headerSize;
|
|
107
|
+
if (!Number.isSafeInteger(codeSize) || codeOffset + codeSize > bytes.length) {
|
|
108
|
+
throw new FormatError("Method code exceeds the file", { offset: codeOffset, context: "method body" });
|
|
109
|
+
}
|
|
110
|
+
const instructions = decodeInstructions(bytes.subarray(codeOffset, codeOffset + codeSize), {
|
|
111
|
+
baseOffset: codeOffset
|
|
112
|
+
});
|
|
113
|
+
let sections = [];
|
|
114
|
+
let endOffset = codeOffset + codeSize;
|
|
115
|
+
const hasSections = format === "fat" && (flags & 0x08) !== 0;
|
|
116
|
+
if (hasSections) {
|
|
117
|
+
let sectionRva = (methodRva + headerSize + codeSize + 3) & ~3;
|
|
118
|
+
let sectionOffset = fileOffset + (sectionRva - methodRva);
|
|
119
|
+
let more = true;
|
|
120
|
+
while (more) {
|
|
121
|
+
if (sectionOffset >= bytes.length) {
|
|
122
|
+
throw new FormatError("Method data section exceeds the file", { offset: sectionOffset, context: "method body" });
|
|
123
|
+
}
|
|
124
|
+
const section = parseExceptionSection(bytes, sectionOffset, sectionRva);
|
|
125
|
+
sections.push(section);
|
|
126
|
+
endOffset = sectionOffset + section.size;
|
|
127
|
+
more = section.moreSections;
|
|
128
|
+
if (more) {
|
|
129
|
+
sectionRva = (sectionRva + section.size + 3) & ~3;
|
|
130
|
+
sectionOffset = fileOffset + (sectionRva - methodRva);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
for (const section of sections) {
|
|
135
|
+
for (const clause of section.clauses) {
|
|
136
|
+
if (clause.tryOffset + clause.tryLength > codeSize ||
|
|
137
|
+
clause.handlerOffset + clause.handlerLength > codeSize) {
|
|
138
|
+
throw new FormatError("Exception-handler region exceeds the method code", {
|
|
139
|
+
offset: section.offset,
|
|
140
|
+
context: "method body"
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if ((clause.flags & 1) !== 0 && clause.classTokenOrFilterOffset >= codeSize) {
|
|
144
|
+
throw new FormatError("Exception filter offset exceeds the method code", {
|
|
145
|
+
offset: section.offset,
|
|
146
|
+
context: "method body"
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
format,
|
|
154
|
+
flags,
|
|
155
|
+
maxStack,
|
|
156
|
+
codeSize,
|
|
157
|
+
localVarSigToken,
|
|
158
|
+
headerOffset: fileOffset,
|
|
159
|
+
headerSize,
|
|
160
|
+
codeOffset,
|
|
161
|
+
endOffset,
|
|
162
|
+
instructions,
|
|
163
|
+
sections
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { BinaryReader } from "../binary/reader.js";
|
|
2
|
+
import { FormatError } from "../binary/errors.js";
|
|
3
|
+
import { getOpcode } from "./opcodes.js";
|
|
4
|
+
|
|
5
|
+
const operandReaders = {
|
|
6
|
+
none: () => undefined,
|
|
7
|
+
u1: (reader) => reader.u1(),
|
|
8
|
+
i1: (reader) => reader.i1(),
|
|
9
|
+
u2: (reader) => reader.u2(),
|
|
10
|
+
i4: (reader) => reader.i4(),
|
|
11
|
+
i8: (reader) => reader.u8(),
|
|
12
|
+
f4: (reader) => reader.f4(),
|
|
13
|
+
f8: (reader) => reader.f8(),
|
|
14
|
+
token: (reader) => reader.u4()
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function decodeInstructions(input, { baseOffset = 0, ilOffset = 0 } = {}) {
|
|
18
|
+
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
19
|
+
const reader = new BinaryReader(bytes, { baseOffset, context: "CIL instructions" });
|
|
20
|
+
const instructions = [];
|
|
21
|
+
while (reader.remaining > 0) {
|
|
22
|
+
const start = reader.position;
|
|
23
|
+
const first = reader.u1();
|
|
24
|
+
const code = first === 0xfe ? 0xfe00 | reader.u1() : first;
|
|
25
|
+
const opcode = getOpcode(code);
|
|
26
|
+
if (!opcode) {
|
|
27
|
+
throw new FormatError(`Unknown CIL opcode 0x${code.toString(16)}`, {
|
|
28
|
+
offset: baseOffset + start,
|
|
29
|
+
context: "CIL instructions"
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const operandOffset = baseOffset + reader.position;
|
|
33
|
+
let operand;
|
|
34
|
+
let patchOffsets = [];
|
|
35
|
+
if (opcode.operand === "branch1" || opcode.operand === "branch4") {
|
|
36
|
+
const displacement = opcode.operand === "branch1" ? reader.i1() : reader.i4();
|
|
37
|
+
operand = { kind: "branch", targetOffset: reader.position + displacement + ilOffset };
|
|
38
|
+
patchOffsets = [operandOffset];
|
|
39
|
+
} else if (opcode.operand === "switch") {
|
|
40
|
+
const count = reader.u4();
|
|
41
|
+
if (count > Math.floor(reader.remaining / 4)) {
|
|
42
|
+
throw new FormatError("Switch target count exceeds remaining method body", {
|
|
43
|
+
offset: baseOffset + start,
|
|
44
|
+
context: "CIL switch operand"
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const offsetsStart = reader.position;
|
|
48
|
+
const displacements = Array.from({ length: count }, () => reader.i4());
|
|
49
|
+
const base = reader.position + ilOffset;
|
|
50
|
+
operand = {
|
|
51
|
+
kind: "switch",
|
|
52
|
+
targetOffsets: displacements.map((value) => base + value)
|
|
53
|
+
};
|
|
54
|
+
patchOffsets = displacements.map((_, index) => baseOffset + offsetsStart + index * 4);
|
|
55
|
+
} else {
|
|
56
|
+
const read = operandReaders[opcode.operand];
|
|
57
|
+
if (!read) throw new FormatError(`Unsupported operand kind ${opcode.operand}`, { offset: baseOffset + start });
|
|
58
|
+
const value = read(reader);
|
|
59
|
+
operand = value === undefined ? null : { kind: opcode.operand, value };
|
|
60
|
+
if (opcode.operand !== "none") patchOffsets = [operandOffset];
|
|
61
|
+
}
|
|
62
|
+
instructions.push({
|
|
63
|
+
opcode,
|
|
64
|
+
offset: start + ilOffset,
|
|
65
|
+
size: reader.position - start,
|
|
66
|
+
opcodeOffset: baseOffset + start,
|
|
67
|
+
opcodeSize: first === 0xfe ? 2 : 1,
|
|
68
|
+
operandOffset,
|
|
69
|
+
patchOffsets,
|
|
70
|
+
operand
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const boundaries = new Set(instructions.map((instruction) => instruction.offset));
|
|
74
|
+
for (const instruction of instructions) {
|
|
75
|
+
const targets = instruction.operand?.kind === "branch"
|
|
76
|
+
? [instruction.operand.targetOffset]
|
|
77
|
+
: instruction.operand?.kind === "switch"
|
|
78
|
+
? instruction.operand.targetOffsets
|
|
79
|
+
: [];
|
|
80
|
+
for (const target of targets) {
|
|
81
|
+
if (!boundaries.has(target)) {
|
|
82
|
+
throw new FormatError(`Branch target IL_${target.toString(16)} is not an instruction boundary`, {
|
|
83
|
+
offset: instruction.operandOffset,
|
|
84
|
+
context: "CIL branch operand"
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return instructions;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function encodeOpcode(opcode) {
|
|
93
|
+
if (!opcode || !Number.isInteger(opcode.code) || opcode.code < 0 || opcode.code > 0xffff) {
|
|
94
|
+
throw new TypeError("Invalid CIL opcode");
|
|
95
|
+
}
|
|
96
|
+
return opcode.code > 0xff
|
|
97
|
+
? Uint8Array.from([0xfe, opcode.code & 0xff])
|
|
98
|
+
: Uint8Array.from([opcode.code]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getInstructionSize(opcode, operand) {
|
|
102
|
+
const opcodeSize = opcode.code > 0xff ? 2 : 1;
|
|
103
|
+
const operandSize = {
|
|
104
|
+
none: 0,
|
|
105
|
+
u1: 1,
|
|
106
|
+
i1: 1,
|
|
107
|
+
u2: 2,
|
|
108
|
+
i4: 4,
|
|
109
|
+
i8: 8,
|
|
110
|
+
f4: 4,
|
|
111
|
+
f8: 8,
|
|
112
|
+
token: 4,
|
|
113
|
+
branch1: 1,
|
|
114
|
+
branch4: 4,
|
|
115
|
+
switch: 4 + 4 * (operand?.targetOffsets?.length ?? 0)
|
|
116
|
+
}[opcode.operand];
|
|
117
|
+
if (operandSize === undefined) throw new FormatError(`Unsupported operand kind ${opcode.operand}`);
|
|
118
|
+
return opcodeSize + operandSize;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function encodeInstruction(instruction) {
|
|
122
|
+
const opcodeBytes = encodeOpcode(instruction.opcode);
|
|
123
|
+
let operandBytes = new Uint8Array();
|
|
124
|
+
if (instruction.operand) {
|
|
125
|
+
const value = instruction.operand.kind === "branch"
|
|
126
|
+
? instruction.operand.targetOffset
|
|
127
|
+
: instruction.operand.kind === "switch"
|
|
128
|
+
? instruction.operand.targetOffsets
|
|
129
|
+
: instruction.operand.value;
|
|
130
|
+
operandBytes = encodeOperand(instruction, value);
|
|
131
|
+
if (instruction.operand.kind === "switch") {
|
|
132
|
+
const count = new Uint8Array(4);
|
|
133
|
+
new DataView(count.buffer).setUint32(0, instruction.operand.targetOffsets.length, true);
|
|
134
|
+
const combined = new Uint8Array(count.length + operandBytes.length);
|
|
135
|
+
combined.set(count);
|
|
136
|
+
combined.set(operandBytes, count.length);
|
|
137
|
+
operandBytes = combined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (opcodeBytes.length + operandBytes.length !== instruction.size) {
|
|
141
|
+
throw new FormatError("Encoded instruction size does not match the layout");
|
|
142
|
+
}
|
|
143
|
+
const result = new Uint8Array(instruction.size);
|
|
144
|
+
result.set(opcodeBytes);
|
|
145
|
+
result.set(operandBytes, opcodeBytes.length);
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function formatInstruction(instruction) {
|
|
150
|
+
const operand = instruction.operand;
|
|
151
|
+
if (!operand) return `${instruction.opcode.name}`;
|
|
152
|
+
if (operand.kind === "branch") return `${instruction.opcode.name} IL_${operand.targetOffset.toString(16).padStart(4, "0")}`;
|
|
153
|
+
if (operand.kind === "switch") {
|
|
154
|
+
return `${instruction.opcode.name} (${operand.targetOffsets.map((target) => `IL_${target.toString(16).padStart(4, "0")}`).join(", ")})`;
|
|
155
|
+
}
|
|
156
|
+
const value = typeof operand.value === "bigint" ? `${operand.value}n` : String(operand.value);
|
|
157
|
+
return `${instruction.opcode.name} ${value}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function encodeOperand(instruction, value) {
|
|
161
|
+
const kind = instruction.operand?.kind;
|
|
162
|
+
const buffer = new ArrayBuffer(instruction.size);
|
|
163
|
+
const view = new DataView(buffer);
|
|
164
|
+
if (kind === "branch") {
|
|
165
|
+
const width = instruction.opcode.operand === "branch1" ? 1 : 4;
|
|
166
|
+
const displacement = value - (instruction.offset + instruction.size);
|
|
167
|
+
if (width === 1) {
|
|
168
|
+
if (!Number.isInteger(displacement) || displacement < -128 || displacement > 127) {
|
|
169
|
+
throw new RangeError("Branch target does not fit the existing short-branch operand");
|
|
170
|
+
}
|
|
171
|
+
view.setInt8(0, displacement);
|
|
172
|
+
return new Uint8Array(buffer, 0, 1);
|
|
173
|
+
}
|
|
174
|
+
if (!Number.isInteger(displacement) || displacement < -0x80000000 || displacement > 0x7fffffff) {
|
|
175
|
+
throw new RangeError("Branch target does not fit the existing branch operand");
|
|
176
|
+
}
|
|
177
|
+
view.setInt32(0, displacement, true);
|
|
178
|
+
return new Uint8Array(buffer, 0, 4);
|
|
179
|
+
}
|
|
180
|
+
if (kind === "switch") {
|
|
181
|
+
if (!Array.isArray(value) || value.length !== instruction.operand.targetOffsets.length) {
|
|
182
|
+
throw new RangeError("A fixed-size edit must preserve the switch target count");
|
|
183
|
+
}
|
|
184
|
+
const baseOffset = instruction.offset + instruction.size;
|
|
185
|
+
const result = new Uint8Array(value.length * 4);
|
|
186
|
+
const switchView = new DataView(result.buffer);
|
|
187
|
+
value.forEach((target, index) => {
|
|
188
|
+
const displacement = target - baseOffset;
|
|
189
|
+
if (!Number.isInteger(target) || displacement < -0x80000000 || displacement > 0x7fffffff) {
|
|
190
|
+
throw new RangeError("Switch target does not fit its 32-bit displacement");
|
|
191
|
+
}
|
|
192
|
+
switchView.setInt32(index * 4, displacement, true);
|
|
193
|
+
});
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
const operandInfo = {
|
|
197
|
+
u1: ["setUint8", 1], i1: ["setInt8", 1], u2: ["setUint16", 2], i4: ["setInt32", 4],
|
|
198
|
+
i8: ["setBigInt64", 8], f4: ["setFloat32", 4], f8: ["setFloat64", 8], token: ["setUint32", 4]
|
|
199
|
+
}[kind];
|
|
200
|
+
if (!operandInfo) throw new TypeError("Instruction does not have a patchable fixed-size operand");
|
|
201
|
+
const [setter, width] = operandInfo;
|
|
202
|
+
if (kind === "token" || kind === "u1" || kind === "u2") {
|
|
203
|
+
const max = width === 1 ? 0xff : width === 2 ? 0xffff : 0xffffffff;
|
|
204
|
+
if (!Number.isInteger(value) || value < 0 || value > max) throw new RangeError(`Value does not fit ${kind}`);
|
|
205
|
+
} else if (kind === "i1" && (!Number.isInteger(value) || value < -128 || value > 127)) {
|
|
206
|
+
throw new RangeError("Value does not fit i1");
|
|
207
|
+
} else if (kind === "i4" && (!Number.isInteger(value) || value < -0x80000000 || value > 0x7fffffff)) {
|
|
208
|
+
throw new RangeError("Value does not fit i4");
|
|
209
|
+
} else if (kind === "i8" && (typeof value !== "bigint" || value < -(1n << 63n) || value > (1n << 63n) - 1n)) {
|
|
210
|
+
throw new RangeError("Value does not fit i8");
|
|
211
|
+
} else if ((kind === "f4" || kind === "f8") && typeof value !== "number") {
|
|
212
|
+
throw new TypeError("Floating-point operands must be numbers");
|
|
213
|
+
}
|
|
214
|
+
const result = new Uint8Array(width);
|
|
215
|
+
new DataView(result.buffer)[setter](0, value, true);
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
function descriptor(code, name, operand = "none", shortBranch = null, longBranch = null) {
|
|
2
|
+
return { code, name, operand, shortBranch, longBranch };
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
const opcodes = new Map();
|
|
6
|
+
const add = (...args) => {
|
|
7
|
+
const entry = descriptor(...args);
|
|
8
|
+
opcodes.set(entry.code, entry);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
add(0x00, "nop");
|
|
12
|
+
add(0x01, "break");
|
|
13
|
+
for (let i = 0; i < 4; i++) {
|
|
14
|
+
add(0x02 + i, `ldarg.${i}`);
|
|
15
|
+
add(0x06 + i, `ldloc.${i}`);
|
|
16
|
+
add(0x0a + i, `stloc.${i}`);
|
|
17
|
+
}
|
|
18
|
+
add(0x0e, "ldarg.s", "u1");
|
|
19
|
+
add(0x0f, "ldarga.s", "u1");
|
|
20
|
+
add(0x10, "starg.s", "u1");
|
|
21
|
+
add(0x11, "ldloc.s", "u1");
|
|
22
|
+
add(0x12, "ldloca.s", "u1");
|
|
23
|
+
add(0x13, "stloc.s", "u1");
|
|
24
|
+
add(0x14, "ldnull");
|
|
25
|
+
add(0x15, "ldc.i4.m1");
|
|
26
|
+
for (let i = 0; i <= 8; i++) add(0x16 + i, `ldc.i4.${i}`);
|
|
27
|
+
add(0x1f, "ldc.i4.s", "i1");
|
|
28
|
+
add(0x20, "ldc.i4", "i4");
|
|
29
|
+
add(0x21, "ldc.i8", "i8");
|
|
30
|
+
add(0x22, "ldc.r4", "f4");
|
|
31
|
+
add(0x23, "ldc.r8", "f8");
|
|
32
|
+
add(0x25, "dup");
|
|
33
|
+
add(0x26, "pop");
|
|
34
|
+
add(0x27, "jmp", "token");
|
|
35
|
+
add(0x28, "call", "token");
|
|
36
|
+
add(0x29, "calli", "token");
|
|
37
|
+
add(0x2a, "ret");
|
|
38
|
+
|
|
39
|
+
const branchNames = ["br", "brfalse", "brtrue", "beq", "bge", "bgt", "ble", "blt", "bne.un", "bge.un", "bgt.un", "ble.un", "blt.un"];
|
|
40
|
+
for (let i = 0; i < branchNames.length; i++) {
|
|
41
|
+
add(0x2b + i, `${branchNames[i]}.s`, "branch1", null, 0x38 + i);
|
|
42
|
+
add(0x38 + i, branchNames[i], "branch4", 0x2b + i);
|
|
43
|
+
}
|
|
44
|
+
add(0x45, "switch", "switch");
|
|
45
|
+
|
|
46
|
+
const simpleNames = [
|
|
47
|
+
"ldind.i1", "ldind.u1", "ldind.i2", "ldind.u2", "ldind.i4", "ldind.u4", "ldind.i8", "ldind.i",
|
|
48
|
+
"ldind.r4", "ldind.r8", "ldind.ref", "stind.ref", "stind.i1", "stind.i2", "stind.i4", "stind.i8",
|
|
49
|
+
"stind.r4", "stind.r8", "add", "sub", "mul", "div", "div.un", "rem", "rem.un", "and", "or", "xor",
|
|
50
|
+
"shl", "shr", "shr.un", "neg", "not", "conv.i1", "conv.i2", "conv.i4", "conv.i8", "conv.r4",
|
|
51
|
+
"conv.r8", "conv.u4", "conv.u8"
|
|
52
|
+
];
|
|
53
|
+
for (let i = 0; i < simpleNames.length; i++) add(0x46 + i, simpleNames[i]);
|
|
54
|
+
|
|
55
|
+
[
|
|
56
|
+
[0x6f, "callvirt", "token"], [0x70, "cpobj", "token"], [0x71, "ldobj", "token"],
|
|
57
|
+
[0x72, "ldstr", "token"], [0x73, "newobj", "token"], [0x74, "castclass", "token"],
|
|
58
|
+
[0x75, "isinst", "token"], [0x76, "conv.r.un"], [0x79, "unbox", "token"],
|
|
59
|
+
[0x7a, "throw"], [0x7b, "ldfld", "token"], [0x7c, "ldflda", "token"],
|
|
60
|
+
[0x7d, "stfld", "token"], [0x7e, "ldsfld", "token"], [0x7f, "ldsflda", "token"],
|
|
61
|
+
[0x80, "stsfld", "token"], [0x81, "stobj", "token"], [0x8c, "box", "token"],
|
|
62
|
+
[0x8d, "newarr", "token"], [0x8e, "ldlen"], [0x8f, "ldelema", "token"],
|
|
63
|
+
[0xa3, "ldelem", "token"], [0xa4, "stelem", "token"], [0xa5, "unbox.any", "token"],
|
|
64
|
+
[0xc2, "refanyval", "token"], [0xc3, "ckfinite"], [0xc6, "mkrefany", "token"],
|
|
65
|
+
[0xd0, "ldtoken", "token"], [0xdc, "endfinally"], [0xdd, "leave", "branch4", 0xde],
|
|
66
|
+
[0xde, "leave.s", "branch1", null, 0xdd], [0xdf, "stind.i"], [0xe0, "conv.u"]
|
|
67
|
+
].forEach((item) => add(...item));
|
|
68
|
+
|
|
69
|
+
const conversionNames = [
|
|
70
|
+
"conv.ovf.i1.un", "conv.ovf.i2.un", "conv.ovf.i4.un", "conv.ovf.i8.un", "conv.ovf.u1.un",
|
|
71
|
+
"conv.ovf.u2.un", "conv.ovf.u4.un", "conv.ovf.u8.un", "conv.ovf.i.un", "conv.ovf.u.un"
|
|
72
|
+
];
|
|
73
|
+
for (let i = 0; i < conversionNames.length; i++) add(0x82 + i, conversionNames[i]);
|
|
74
|
+
for (let i = 0; i < 11; i++) add(0x90 + i, `ldelem.${["i1", "u1", "i2", "u2", "i4", "u4", "i8", "i", "r4", "r8", "ref"][i]}`);
|
|
75
|
+
for (let i = 0; i < 7; i++) add(0x9b + i, `stelem.${["i", "i1", "i2", "i4", "i8", "r4", "r8"][i]}`);
|
|
76
|
+
add(0xa2, "stelem.ref");
|
|
77
|
+
for (const [code, name] of [
|
|
78
|
+
[0xb3, "conv.ovf.i1"], [0xb4, "conv.ovf.u1"], [0xb5, "conv.ovf.i2"], [0xb6, "conv.ovf.u2"],
|
|
79
|
+
[0xb7, "conv.ovf.i4"], [0xb8, "conv.ovf.u4"], [0xb9, "conv.ovf.i8"], [0xba, "conv.ovf.u8"],
|
|
80
|
+
[0xd1, "conv.i"], [0xd2, "conv.ovf.i"],
|
|
81
|
+
[0xd3, "conv.ovf.u"], [0xd4, "add.ovf"], [0xd5, "add.ovf.un"], [0xd6, "mul.ovf"],
|
|
82
|
+
[0xd7, "mul.ovf.un"], [0xd8, "sub.ovf"], [0xd9, "sub.ovf.un"]
|
|
83
|
+
]) add(code, name);
|
|
84
|
+
|
|
85
|
+
for (const [code, name, operand] of [
|
|
86
|
+
[0xfe00, "arglist"], [0xfe01, "ceq"], [0xfe02, "cgt"], [0xfe03, "cgt.un"],
|
|
87
|
+
[0xfe04, "clt"], [0xfe05, "clt.un"], [0xfe06, "ldftn", "token"], [0xfe07, "ldvirtftn", "token"],
|
|
88
|
+
[0xfe09, "ldarg", "u2"], [0xfe0a, "ldarga", "u2"], [0xfe0b, "starg", "u2"],
|
|
89
|
+
[0xfe0c, "ldloc", "u2"], [0xfe0d, "ldloca", "u2"], [0xfe0e, "stloc", "u2"],
|
|
90
|
+
[0xfe0f, "localloc"], [0xfe11, "endfilter"], [0xfe12, "unaligned.", "u1"],
|
|
91
|
+
[0xfe13, "volatile."], [0xfe14, "tail."], [0xfe15, "initobj", "token"],
|
|
92
|
+
[0xfe16, "constrained.", "token"], [0xfe17, "cpblk"], [0xfe18, "initblk"],
|
|
93
|
+
[0xfe1a, "rethrow"], [0xfe1c, "sizeof", "token"], [0xfe1d, "refanytype"], [0xfe1e, "readonly."]
|
|
94
|
+
]) add(code, name, operand ?? "none");
|
|
95
|
+
|
|
96
|
+
export function getOpcode(code) {
|
|
97
|
+
return opcodes.get(code) ?? null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function listOpcodes() {
|
|
101
|
+
return [...opcodes.values()];
|
|
102
|
+
}
|