-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patharmlint.h
More file actions
2881 lines (2747 loc) · 160 KB
/
Copy patharmlint.h
File metadata and controls
2881 lines (2747 loc) · 160 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright 2026 Andrew Gaul <andrew@gaul.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef ARMLINT_H
#define ARMLINT_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <capstone/capstone.h>
#ifdef __cplusplus
extern "C" {
#endif
// Pure predicate: true iff imm is encodable as an AArch64 logical
// (bitmask) immediate at the given register width (32 or 64).
//
// Exposed for direct testing. The encoding excludes 0 and the
// all-ones value at the given width.
bool is_bitmask_immediate(uint64_t imm, unsigned reg_width);
// NZCV flag-liveness classification of a single 32-bit A64 instruction
// word, used by the forward scan that drops a CMP/TST once the flags are
// provably dead. Exposed so the test suite can cross-validate it against
// Capstone's register-access model (test_liveness_matches_capstone).
//
// The classifier is hand-rolled rather than derived from Capstone because
// Capstone's implicit-flag model is incomplete: 5.0.x cs_regs_access
// records none of the NZCV reads of BC.cond, MRS NZCV, CFINV/XAFLAG/
// AXFLAG, RMIF, SETF8/SETF16, or the FEAT_MOPS main/epilogue stages
// (which it decodes but models no flag access for), and 6.0 still
// misses MRS NZCV -- precisely the readers that must not be dropped.
// The cross-check therefore treats Capstone as a one-directional partial
// oracle (see the test for the exact properties).
//
// The register-side scan (classify_reg_liveness) does trust Capstone's
// operand access flags, and has its own correction for the same reason.
// Capstone drops the XZR destination of CMP/CMN/TST and leaves the first
// SOURCE operand in slot 0, where it marks it CS_AC_WRITE -- the
// convention for slot 0. Taken at face value that turns every compare of
// a watched register into a proof that the register is dead, so
// insn_writes_no_gpr recognizes the S-variant forms whose Rd is 31 and
// demotes their operands back to reads.
//
// The mirror case is a read Capstone reports and armlint throws away.
// insn_reg_access honors a read flag on a WRITTEN operand only when
// insn_reads_gpr_dest confirms the encoding really is a
// read-modify-write, because Capstone marks whole encoding classes
// read+write. That list has to name every genuine RMW: MOVK, BFM, and
// the pointer authentication transforms, which sign, authenticate or
// strip Rd in place.
typedef enum {
LIV_UNKNOWN, // no effect on NZCV; keep scanning
LIV_OVERWRITE, // writes all NZCV without reading them first
LIV_READ, // reads any of NZCV
LIV_TERM_SAFE, // terminator after which prior NZCV is unobservable
LIV_TERM_UNSAFE, // terminator whose target may observe NZCV
} liveness_t;
liveness_t classify_liveness(uint32_t op);
// The register-side twin of classify_liveness: how a single instruction
// affects the liveness of ONE general-purpose register, used by the
// forward scans that prove a producer's destination dead before
// deleting it. Exposed for the same reason -- so the test suite can
// cross-validate it against Capstone (test_reg_liveness_matches_capstone
// and the exhaustive sweep beside it).
//
// The register side trusts Capstone more than the NZCV side does: it
// takes the operand access flags rather than re-deriving them, with
// two corrections applied in insn_reg_access for the places 5.x gets
// them wrong. That makes the cross-check MORE valuable here, not less
// -- the classifier and its oracle share a source, so a disagreement
// is nearly always a real defect on one side. Both corrections were
// found that way.
//
// `reg` is a 0..30 GPR encoding number, as returned by arm64_gpr_num.
liveness_t classify_reg_liveness(const cs_insn *insn, int reg);
// Map a Capstone AArch64 register id to its 0..30 GPR encoding number,
// or -1 for the zero register, SP, and every non-GPR. Exposed so the
// cross-check can walk Capstone's register lists in armlint's own
// numbering rather than reimplementing the mapping and drifting from
// it.
int arm64_gpr_num(unsigned reg);
// State carried across instructions for sequence-based checks. Owned by
// the caller; created once per scan and reset between non-contiguous
// regions (e.g. between executable sections).
typedef struct armlint_state armlint_state;
armlint_state *armlint_state_create(void);
void armlint_state_destroy(armlint_state *state);
void armlint_state_reset(armlint_state *state);
// ISA-extension feature bits for armlint_state_set_features. Checks
// that suggest extension instructions stay silent unless their
// feature is enabled (the CLI maps -m cssc etc. onto these).
#define ARMLINT_FEATURE_CSSC (1u << 0)
#define ARMLINT_FEATURE_LRCPC2 (1u << 1)
#define ARMLINT_FEATURE_PAUTH (1u << 2)
#define ARMLINT_FEATURE_LSE (1u << 3)
// Bit 6, not 4: the two V8 knowledge bits below already claimed 4
// and 5, and their values are part of the shipped header's contract.
#define ARMLINT_FEATURE_CMPBR (1u << 6)
#define ARMLINT_FEATURE_SHA3 (1u << 7)
// FEAT_FP16 supplies the half-precision transfer forms, notably
// FMOV Wd, Hn -- the target of the halfword arm of the lane-0 UMOV
// fold. Without it that arm stays silent; the S and D arms need no
// extension and are always live.
#define ARMLINT_FEATURE_FP16 (1u << 8)
// V8CAGE differs in kind from the ISA bits above: it asserts a runtime
// invariant of the scanned code rather than a hardware capability --
// that x28 holds V8's pointer-compression cage base, which is 4GB
// aligned so its low 32 bits are zero. The checks it gates are unsound
// for arbitrary code and stay silent without it.
#define ARMLINT_FEATURE_V8CAGE (1u << 4)
// V8POOL is a knowledge bit like V8CAGE, but about the instruction
// stream's shape rather than a register invariant: V8's JIT opens each
// inline constant pool with a self-describing marker -- LDR XZR,
// (literal) whose imm19 counts the 32-bit data words that follow
// (guard, alignment, and constants; the marker itself is not counted).
// Under this bit every scan loop skips the marker and its data instead
// of decoding embedded constants as instructions, and the branch-target
// map ignores them. Unsound for arbitrary code, where a literal load to
// XZR is a legal (if pointless) discarded load followed by real
// instructions; enable it only for V8 JIT dumps (v8dump2elf output).
#define ARMLINT_FEATURE_V8POOL (1u << 5)
// Audit bits, kept in the high half of the same features word. They
// are different in kind from the ISA bits above: -m asserts what the
// target supports so rewrites may use it, while an audit opts into
// informational checks that flag missing hardening rather than a
// missed fold. Audit findings are review items -- a benign residue
// (jump tables under the PAC audit) is expected in the output.
#define ARMLINT_AUDIT_PAC (1u << 16)
// Enable ISA-extension-gated checks (a bitmask of ARMLINT_FEATURE_*).
void armlint_state_set_features(armlint_state *state, unsigned features);
// Give binary-aware checks access to the full scanned buffer (the
// bytes the driver is disassembling, offset 0 = the first byte).
// Checks that chase PC-relative data -- literal pools -- read from
// it; without a buffer those checks stay silent. Setting the buffer
// also runs a one-pass scan of its direct branches (B/BL, B.cond,
// CBZ/CBNZ, TBZ/TBNZ) to build the branch-target map that gates the
// 2->1 memory-op folds against side entries; without a buffer that
// gate stays off. The pointer must outlive the scan. That branch scan
// honors the state's feature bits (ARMLINT_FEATURE_V8POOL skips V8
// constant pools), so set features before setting the buffer.
void armlint_state_set_buffer(armlint_state *state, const uint8_t *buf,
size_t len);
// Findings reported by check functions. start_offset is the offset of
// the first instruction of the suboptimal run; insn_count is its length.
// detail is a short summary (e.g. the constructed constant, or the
// suggested folded instruction) shown in the header. lines[] holds the
// disassembled offending instructions, one per line; unused slots are
// empty strings.
//
// Checks own the formatting of their detail and lines so that reporting
// does not need to retain the original cs_insn array (cs_disasm_iter
// recycles a single cs_insn slot).
#define ARMLINT_FINDING_LINES 5
#define ARMLINT_FINDING_LINE_LEN 96
#define ARMLINT_FINDING_DETAIL_LEN 128
typedef struct {
const char *name;
size_t start_offset;
unsigned insn_count;
char detail[ARMLINT_FINDING_DETAIL_LEN];
char lines[ARMLINT_FINDING_LINES][ARMLINT_FINDING_LINE_LEN];
} armlint_finding;
// Shared signature for per-instruction checks and pre-instruction
// pending-state advancers. Each returns true and fills *out when it
// produces a finding for the given instruction; the advancers ignore
// offset (their finding's offset was recorded when the deferred state
// was opened).
typedef bool (*armlint_check_fn)(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Ordered table of every check the driver runs per instruction. The
// pre-instruction advancers (armlint_advance_pending*) appear at the
// front of the list and must run before the per-instruction checks --
// see the comment on armlint_advance_pending for why ordering matters.
// Iterate from 0 to armlint_check_registry_count - 1; both
// check_instructions (driver) and the test harness drive findings off
// this single list, so adding a check requires editing only one place.
// Consumers must drop any finding for which
// armlint_finding_has_side_entry() returns true before reporting it;
// the driver and the test harness both do.
extern const armlint_check_fn armlint_check_registry[];
extern const size_t armlint_check_registry_count;
// True when some instruction of the finding's window AFTER the first
// is the target of a direct branch -- a side entry into the rewritten
// window, which invalidates any multi-instruction rewrite because the
// entering path skips the window's head. Registry consumers drop such
// findings at emission. Requires the branch-target map built by
// armlint_state_set_buffer; without a buffer this always returns
// false (single-instruction findings are immune either way).
bool armlint_finding_has_side_entry(const armlint_state *state,
const armlint_finding *finding);
// Track MOVZ/MOVN + MOVK chains and flag one whose final value is
// reachable more cheaply: either as a single bitmask-immediate MOV
// (ORR Rd, ZR, #imm), or by a shorter move-wide sequence -- the
// minimal length is one instruction per non-zero halfword for a
// MOVZ-based chain and one per non-0xFFFF halfword for a MOVN-based
// chain (each with a floor of one), whichever is smaller. Despite the
// name, the bitmask immediate is just one of the two rewrites.
//
// May produce a finding when a non-matching instruction closes a
// previously open sequence, so callers must invoke armlint_flush
// after the last instruction of a region to catch a trailing
// sequence.
bool check_movz_movk_bitmask(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect a shift (immediate) -- LSL/LSR/ASR, or ROR via the
// same-register EXTR alias -- immediately followed by an arithmetic or
// logical shifted-register op that consumes the shift's destination.
// The pair can be replaced by a single shifted-register form (the
// shift rides on the consumer's Rm). ROR folds only into the logical
// consumers: the arithmetic shifted-register encoding reserves shift
// type 11.
//
// The rewrite deletes the shift, so its destination must be dead
// afterward: a consumer that overwrites it proves that on the spot,
// and one writing a fresh register defers through the forward
// register-liveness scan (see armlint_advance_pending_mz). Rd = 31
// consumers are excluded -- the non-S forms are dead writes, the S
// forms the CMP/CMN/TST aliases.
//
// The shift result may sit in the consumer's Rm slot (any consumer) or
// its Rn slot (commutative consumers only -- ADD/ADDS/AND/ANDS/ORR/EOR
// and EON, which is XNOR; the fold swaps the two sources so the shifted
// value lands on Rm). The other ("independent") source operand must not
// also be the shift destination: with both sources equal to it (e.g.
// `lsl wt,ws,#k ; add wt,wt,wt`) the rewrite would read a stale
// pre-shift value, so that case is rejected. An XZR independent
// operand is likewise rejected -- such consumers are shifted register
// copies or constants, not ops the shift rides into.
bool check_lsl_fold(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect an immediate LSL/LSR shift immediately followed by an ORR/EOR/ADD
// whose Rm carries the complementary shift (opposite direction, amounts
// summing to the register width) and whose Rn is the shift's destination.
// The pair is a funnel shift and folds to a single EXTR Rd, Rhi, Rlo, #lsb
// (where the LSR'd source is the low half and the LSL'd source the high
// half); when both funnel sources are the same register it is a rotate and
// folds to ROR Rd, Rs, #lsb. Only ADD/ORR/EOR qualify -- the two shifted
// fields are disjoint, so all three agree with EXTR bit-for-bit -- and the
// consumer's shift must be logical (LSL/LSR), never ASR (its sign fill would
// collide with the other field). Like check_lsl_fold, the rewrite deletes
// the shift, so its destination must be dead afterward: a consumer that
// overwrites it proves that on the spot, and one writing a fresh register
// defers through the forward register-liveness scan (Rd = 31 is a dead
// write and excluded); the inline-shifted source must be a different
// register so the shift does not clobber it.
bool check_funnel_to_extr(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect a zero test of Rn -- any of CMP Rn, #0 / CMP Rn, ZR /
// CMN Rn, #0 / CMN Rn, ZR / TST Rn, Rn (all Rd=31 aliases that leave
// Z = (Rn == 0)) -- immediately followed by B.EQ or B.NE; the pair is
// replaceable by CBZ Rn / CBNZ Rn with the same branch target. The
// unsigned B.HI / B.LS fold the same way after the SUBS-based zero
// tests (CMP Rn, #0 / CMP Rn, ZR): subtracting zero never borrows, so
// C is known set and HI (C && !Z) reduces to NE, LS (!C || Z) to EQ.
// The TST Rn, Rn and CMN forms are excluded from the hi/ls fold --
// ANDS clears C, and adding zero never carries, making HI never taken
// and LS always taken for those spellings. Emission is deferred via
// the pending-finding mechanism so the rewrite is only suggested when
// downstream code provably does not observe the dropped NZCV state --
// see armlint_advance_pending.
bool check_cmp_zero_branch(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect TST Rn, #(1<<k) (ANDS XZR, Rn, #imm) immediately followed by
// B.EQ or B.NE; the pair is replaceable by TBZ Rn, #k or TBNZ Rn, #k
// when the branch target fits in TBZ's shorter (14-bit signed) range.
// Like check_cmp_zero_branch, emission is deferred until the forward
// liveness scan confirms NZCV is dead.
bool check_tst_branch(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect TST Rn, #(1<<k) immediately followed by CSET or CSETM of a
// condition the masked bit fully determines; the pair is a flag-free
// single-instruction bit extract:
// tst w0, #0x10 ; cset w8, ne -> ubfx w8, w0, #4, #1
// tst w0, #0x10 ; csetm w8, ne -> sbfx w8, w0, #4, #1
// NE folds directly (Z clear <=> the bit is set); MI is accepted as
// its synonym when the isolated bit is the producer's sign bit (N is
// that bit). EQ/PL would need an inverted extract (no single
// instruction), and every other condition is constant after TST
// (C = V = 0) -- all are skipped.
//
// CSET's 0/1 result zero-extends identically at either width, so any
// W/X producer/consumer combination folds (the extract renders at the
// consumer's width, bumped to X for bits above 31). CSETM's all-ones
// must replicate at its own width, so a W-form CSETM after an X TST
// of a high bit is skipped. Reads the tst_* pair state owned by
// check_tst_branch (this check runs first in the registry and does
// not expire it).
//
// The rewrite deletes the TST and writes no flags, so all four NZCV
// flags disappear; emission defers through the forward flag-liveness
// scan (armlint_advance_pending) until NZCV is provably dead, exactly
// like the TBZ folds.
bool check_tst_cset(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Flag-free counterpart of check_tst_branch: a producer that isolates
// a single bit k of Rs into Rd -- a non-flag-setting AND with a
// one-bit mask, or a one-bit UBFM/SBFM extract (`ubfx/sbfx Rd, Rs,
// #k, #1`, including the `lsr/asr Rd, Rs, #(datasize-1)` sign-bit
// aliases) -- immediately followed by CBZ/CBNZ of Rd. Rd == 0 iff
// Rs[k] == 0, so the pair folds to TBZ/TBNZ Rs, #k with the same
// target. No NZCV is involved on either side.
//
// The rewrite deletes the producer, so the masked temp Rd must be
// dead afterward -- on BOTH edges of the branch. Emission is deferred
// (armlint_advance_pending_tb): the forward register-liveness scan
// proves the fall-through path (Rd overwritten before any read or
// control transfer), and the taken path is covered by containment --
// the finding is emitted only when the branch target lies within
// [fall-through, kill], the span the scan just proved free of reads
// and control transfers, so the taken edge enters that clean span and
// reaches the same kill. This is the canonical skip-a-small-block
// shape; if/else diamonds whose kill precedes the join are left
// unflagged by the deleting grade.
//
// When the deleting proof cannot hold -- the temp read again, a
// control transfer, window expiry, a backward or uncontained target,
// or end of region -- the pair downgrades instead of dropping: the
// branch alone rebinds to a TBZ/TBNZ of the producer's SOURCE bit at
// its own position and the producer stays ("CBZ/CBNZ of a live
// single-bit test foldable to TBZ/TBNZ"). The rebind holds regardless
// of the producer's liveness (a self-masked AND still carries the
// isolated bit unchanged), so it needs no proof at all; only the
// branch's own displacement (imm19 units) must fit the signed 14-bit
// TBZ encoding. This is the shape a compiler emits for Java-style
// `(flags & F) == 0` tests whose masked temp it re-tests with CBZ.
//
// A W-form CBZ after a producer isolating bit >= 32 is rejected (the
// branch cannot observe the bit -- degenerate). ANDS producers are
// excluded (deleting one loses the NZCV write), as are ZR sources
// (constant branches) and ZR destinations. The deleting grade's TBZ
// displacement (imm19 + 1 units) is range-checked against the signed
// 14-bit encoding, though the containment gate restricts it far more
// tightly in practice.
bool check_single_bit_cbz(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect CSET Rd, cond (CSINC Rd, ZR, ZR, invert(cond)) immediately
// followed by one of three consumers of Rd -- each expressible as a
// single instruction reading the same still-live NZCV, making the
// CSET deletable:
// cset w8, eq ; cbnz w8, L -> b.eq L (CBZ inverts: b.ne L)
// cset w8, eq ; eor w9, w8, #1 -> cset w9, ne
// cset w8, eq ; neg w9, w8 -> csetm w9, eq
// The temp is 0 or 1 zero-extended across the full X register, so a
// consumer of either width observes the same truth value and all
// width combinations fold; the rewrite takes the consumer's width.
// Raw CSINC condition fields AL/NV (a constant 0, not a conditional)
// and ZR destinations are excluded, as are EOR immediates other than
// 1, EOR writing SP (Rd = 31 in a logical immediate), shifted or
// flag-setting NEG forms, and NEG/EOR discarding to ZR.
//
// The branch consumer also covers TBZ/TBNZ #0 of the temp (bit 0 of
// a 0/1 value is its truth value) under its own finding names.
//
// Deleting the CSET requires the temp dead afterward. For the branch
// consumer that means dead on BOTH edges: emission defers through the
// two-edge scan (armlint_advance_pending_tb) built for the single-bit
// fold -- the forward scan proves the fall-through path and the taken
// edge is covered by containment in [fall-through, kill]. B.cond's
// displacement (imm19 + 1 units, replacing the producer) is
// range-checked at the positive encoding limit. When the deleting
// proof cannot hold -- a backward or uncontained target, the temp
// read again, a control transfer, scan-window expiry, end of region,
// or the displacement limit -- the branch consumer downgrades instead
// of dropping: the branch alone is rebound to B.cond and the CSET
// kept ("... of a live CSET foldable to B.cond"), which is valid for
// the adjacent pair regardless of liveness since CSET preserves the
// flags. This is the shape a compiler emits for a multi-use condition
// it must materialize anyway (ART's arm64 backend re-tested the CSET
// output with CBNZ until its HIf codegen learned to branch on the
// still-live flags). For EOR/NEG, a consumer overwriting the temp
// itself kills it on the spot (emit immediately); otherwise emission
// defers through the plain forward scan (defer_dead_mov), downgrading
// on a failed proof just like the branch consumer: the EOR (NEG)
// alone becomes an independent inverted CSET (CSETM) off the
// still-valid flags with the original CSET kept ("... of a live CSET
// foldable to ...").
bool check_cset_fold(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect CMP Rn, #0 immediately followed by a CSET or CSETM that
// materializes the compared register's sign -- a single shift of the
// sign bit:
// cmp x1, #0 ; cset x0, lt -> lsr x0, x1, #63 (w: #31)
// cmp x1, #0 ; csetm x0, mi -> asr x0, x1, #63 (w: #31)
// Subtracting zero can neither borrow nor overflow, so the compare
// leaves N = sign(Rn), Z = (Rn == 0), C = 1, V = 0: conditions LT
// (N != V) and MI (N) both hold exactly when Rn is negative. CSET
// writes that bit as 0/1 (LSR of the sign bit); CSETM writes it
// replicated as 0/all-ones (ASR). The GE/PL complements need a second
// instruction (EOR #1 / MVN) and stay unflagged. The widths must
// agree: an X CSETM after a W compare is a 64-bit mask no single
// W-form shift produces (the sound-but-fiddly mixed CSET cases are
// gated with it). Rn = 31 compares SP, which the shift cannot name;
// AL/NV condition fields and ZR destinations are excluded by the
// CSET/CSETM decode. The rewrite DELETES the compare and sets no
// flags, so emission defers until NZCV is provably dead
// (armlint_advance_pending_sgn). Reported as "CMP #0 + sign
// CSET/CSETM foldable to LSR/ASR".
bool check_cmp_cset_sign(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect the split pointer-authentication epilogue: AUTIASP or
// AUTIBSP immediately followed by a plain RET. The Armv8.3 combined
// forms do both steps in one instruction -- same key (IA/IB), same
// modifier (SP), same register (x30):
// autiasp ; ret -> retaa
// autibsp ; ret -> retab
// Compilers emit the split spelling when the binary must also run on
// pre-Armv8.3 cores: the hint-space AUT executes as a NOP there,
// while the combined forms are UNDEFINED -- which is also why the
// check is gated on ARMLINT_FEATURE_PAUTH (-m pauth). arm64e mandates
// FEAT_PAuth, so the driver arms that flag automatically on arm64e
// Mach-O slices (see scan_macho in main.c); plain arm64 keeps it
// opt-in. Fine print:
// the combined forms do not write the authenticated address back to
// x30, so after the return it holds the still-signed value where the
// split form left the raw one; AAPCS64 makes x30 a plain temporary
// once the call returns, so no conforming caller observes the
// difference (the register twin of the BL-clobbers-NZCV liveness
// rule). On a forged return address both spellings deny the hijack;
// only the diagnosis point differs (FEAT_FPAC faults the standalone
// AUT, the combined form faults given FEAT_FPACCOMBINE, and earlier
// cores branch to a poisoned address either way). RET Xn (n != 30),
// the zero-modifier AUTIAZ/AUTIBZ (no combined return exists), and
// the general-encoding AUTIA x30, SP spelling (unseen in compiler
// output) do not fold. A RET that is itself a branch target -- a
// shared epilogue whose other paths skip the AUT -- is suppressed by
// the central side-entry gate. Reported as "AUTIASP/AUTIBSP + RET
// foldable to RETAA/RETAB (PAuth)".
bool check_aut_ret(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect BR x30: an indirect branch through the link register is a
// spelled-out return. The transfer is architecturally identical to
// RET -- same target, no flags, no writes -- and the sole difference
// is the branch-type hint, which the return-address predictor keys
// on: RET pops the prediction stack the matching BL pushed, while BR
// is predicted as an ordinary indirect branch and desynchronizes
// that stack for the surrounding call tree (the Neoverse and Apple
// optimization guides both state the rule directly: returns use
// RET). A 1-for-1 rewrite with nothing to prove -- no flags, no
// liveness, and a single-instruction finding is sound from any
// entry. Under BTI the rewrite only relaxes the target's landing-pad
// requirement (RET is exempt); on FEAT_GCS hardware a genuine return
// must be RET anyway (BR x30 bypasses the shadow-stack pop, and code
// doing that deliberately deserves the reviewer's eye this finding
// draws). The authenticated BRAA/BRAAZ forms differ in encoding and
// never match; applying the rewrite turns `autiasp ; br x30` into
// the split epilogue the -m pauth fold then takes to RETAA. Reported
// as "BR x30 foldable to RET".
bool check_br_x30(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect a direct branch whose target is the instruction immediately
// after it: control arrives there whether or not the branch is
// taken, so the instruction is a pure no-op. B, B.cond/BC.cond,
// CBZ/CBNZ, and TBZ/TBNZ write no register and no flags, which makes
// deletion sound with no condition or liveness reasoning at all --
// both outcomes fall through. BL is the one deliberate exclusion: it
// writes x30 even over a zero-length span, and `bl .+4` is the
// classic get-the-PC idiom, so deleting it would break its actual
// purpose. The distance test is exactly imm == 1 (in instruction
// units); imm == 0 is branch-to-self, a spin loop, not a no-op.
// Deleting a branch that is itself a branch target is sound -- the
// entering path falls through to the same successor. The shape
// survives in binaries through JIT emitters and patchers,
// empty-block artifacts, and refactored hand assembly. Reported as
// "branch to the next instruction is a no-op".
bool check_branch_to_next(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect the Armv8.0 exclusive-monitor retry loop and suggest the
// single Armv8.1 FEAT_LSE atomic (-m lse). Three shapes, matched
// with strict adjacency plus exact branch targets:
// L: ldxr x8, [x0] L: ldxr x8, [x0]
// add x9, x8, x1 stxr w10, x1, [x0]
// stxr w10, x9, [x0] cbnz w10, L
// cbnz w10, L -> swp x1, x8, [x0]
// -> ldadd x1, x8, [x0]
// L: ldxr x8, [x0]
// cmp x8, x1
// b.ne E -> mov x8, x1
// stxr w10, x2, [x0] cas x8, x2, [x0]
// cbnz w10, L cmp x8, x1
// E:
// The fetch-op's middle op picks the atomic: ADD -> LDADD,
// ORR -> LDSET, EOR -> LDEOR, BIC -> LDCLR (all direct),
// AND -> MVN + LDCLR, SUB -> NEG + LDADD, and ADD/SUB #imm12 ->
// MOV #imm + LDADD; the pre-op's scratch reuses the loop's
// computed-value register, which the rewrite proves dead anyway.
// Commutative ops accept either operand order; SUB/BIC need the
// loaded value on the left. The exclusive pair's ordering carries
// over exactly: LDAXR contributes the A suffix, STLXR the L. Every
// form is the official compiler mapping of the same C11 atomic op
// (GCC and LLVM emit the loop at -march=armv8-a and the LSE form at
// armv8.1+), which is the equivalence argument; the LSE forms are
// also wait-free where the loop can livelock under contention.
// Soundness gates: the fetch-op rewrite writes neither the computed
// value nor the store-exclusive status, so BOTH must be proven dead
// by the dual-register forward scan (armlint_advance_pending_lse);
// the swap and CAS shapes watch only the status, since SWP and CAS
// themselves produce the old value. The registers must be pairwise
// sane -- address and operand loop-invariant (not written inside
// the loop), no SP base, no ZR participants except the CAS shape's
// comparand and stored value (gc compares against WZR and stores
// WZR routinely), in-place computation (Rnew == Rold) accepted for
// the direct ops and excluded for the scratch-using ones (the
// scratch would collide with the atomic's own destination, a
// CONSTRAINED UNPREDICTABLE encoding). The CBNZ tests the status
// register at either width (the status write zero-extends) with
// target exactly the LDXR; a branch into the loop interior is
// suppressed by the central side-entry gate (entry at the LDXR
// itself is fine -- that is the loop's own back edge).
// CAS specifics: the CMP must be the shifted-register form at the
// exclusives' width (W compare over W exclusives is exact; an X
// compare over W exclusives would let the comparand's high bits
// veto a store that CAS would perform, and byte/half loops compare
// through a zero-extended 32-bit CMP that CASB/CASH cannot express,
// so only word and doubleword sizes match), the B.NE must exit to
// exactly the instruction after the CBNZ (both paths converge
// there, so one death scan covers both), and the status register
// may alias the loaded register (gc reuses REGTMP for both). The
// three-instruction rewrite MOV + CAS + CMP preserves the loop's
// entire visible exit state: CAS leaves the old value in the MOV'd
// scratch exactly where the loop left it, and the trailing CMP
// (operand order mirrored from the original) recomputes the same
// NZCV, so only the status register needs a death proof. The
// early-exit path also leaves the exclusive monitor armed where the
// rewrite does not, which well-formed code cannot observe (a STXR
// without a paired LDXR is CONSTRAINED UNPREDICTABLE). Diverging
// early exits (LLVM's CLREX tail), immediate-form comparands, the
// CBNZ-as-compare zero-expected shape, and MIN/MAX-shaped loops
// are recorded in TODO.md. Reported as "LDXR/STXR loop foldable to
// LSE atomic (LSE)".
bool check_lse_rmw(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Dual-register death advancer for the deferred LSE-loop finding:
// commits once every watched register (the loop's computed value and
// store-exclusive status; the swap and CAS shapes watch the status
// only) is overwritten, discards on any read or control transfer,
// parallel to armlint_advance_pending_mz.
bool armlint_advance_pending_lse(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// PAC audit (opt-in, ARMLINT_AUDIT_PAC / -a pac): flag a return
// address spilled to the stack unsigned. pac-ret exists because a
// spilled x30 is the classic ROP target -- sign it before it leaves
// the register file and a stack overwrite faults at authentication
// instead of steering the return. A signed prologue therefore opens
// with PACIASP/PACIBSP before its save of x30; this check reports
// any SP-based x30 spill (STP pre-index or signed offset with either
// data register x30, STR unsigned offset or pre-index) with no
// signing hint in the same straight-line run: a 16-instruction
// window, reset by any control transfer, because real prologues sign
// first and never branch between the signing and the save (interposed
// callee-saved pairs are covered by the window). Leaf functions
// never spill x30 and so produce no findings. Out of scope: non-SP
// bases (a jmp_buf in setjmp is a real PAC surface but a different
// shape) and STP post-index (not a prologue store). The flag asserts
// the full-PAC contract; over a binary that never opted into pac-ret
// it reports every function's spill, by design -- which is why the
// driver arms this audit automatically only on arm64e Mach-O slices
// (which did opt in) and leaves plain arm64 to an explicit -a pac
// (see scan_macho in main.c). Reported as
// "LR spill without PACIASP/PACIBSP (PAC audit)".
bool check_pac_lr_spill(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// PAC audit twin for indirect control flow: flag every plain BR and
// BLR. In fully signed code, function-pointer transfers go through
// BRAA(Z)/BLRAA(Z), which authenticate the target register before
// branching; each raw BR/BLR is a JOP hazard. The output is an
// auditor's worklist, not an error list, and a jump-table classifier
// keeps compiler switch dispatch off it: the clang idiom
// adrp xB,#pg ; add xB,xB,#off ; ldrsw xE,[xB,xI,lsl #2] ;
// adr xA,#. ; add xT,xA,xE ; br xT
// computes its target as a PC-relative base plus a signed offset read
// from a table at a statically materialized (read-only) address --
// not a corruptible pointer -- so a BR to exactly that xT is
// dismissed. The match is strict-adjacency and conservative: the
// dangerous direction for an audit is hiding a real hazard, so only
// this exact five-producer shape is recognized. A BLR is never
// dismissed (a call has no jump-table form), nor is any BR whose
// register was not just computed by the idiom -- so linker veneers
// (adr+br, no table load) and the compact ldrb-scaled table variant
// stay flagged, as does any genuinely unclassified branch. The
// authenticated variants and RET differ in encoding and never match.
// Reported as "unauthenticated BR/BLR (PAC audit)".
bool check_pac_raw_indirect(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// The next rung of the same audit ladder: flag the zero-discriminator
// authenticated branches BRAAZ/BRABZ and BLRAAZ/BLRABZ. These
// authenticate their target, but against the constant-zero modifier,
// so the signature proves only "some pointer signed with this key and
// discriminator zero" -- and on arm64e that class is enormous, since
// the C ABI signs every plain function pointer IA+0 (the census shape:
// a loaded pointer, a NULL check, then BLRAAZ; or a signed epilogue
// tail-calling through BRAAZ). An attacker who can write the slot
// substitutes any other IA+0-signed pointer in the process, so this is
// the weakest live PAC form: one rung above raw BR/BLR (which prove
// nothing) and one below the diversified BRAA/BLRAA Xn, Xm forms,
// whose modifier -- typically the pointer's storage address, per
// __ptrauth address diversity -- narrows the substitution class to
// pointers signed for that one slot. Those diversified forms set
// Z = 1 (bit 24) and never match here, including with Xm = SP; RETAA/
// RETAB (SP-diversified by construction) are a different encoding
// entirely. Findings are worklist items, not errors: IA+0 is the ABI
// floor for interchangeable C function pointers, and each site is a
// candidate for a __ptrauth-qualified upgrade rather than a bug.
// Reported as "zero-discriminator authenticated BR/BLR (PAC audit)".
bool check_pac_zero_disc_indirect(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect a producer that provably zeros bits 63..P of its destination,
// immediately followed by an in-place zero-extension consumer that
// clears bits >= C with P <= C -- a no-op. Producers: any W-form
// data-processing write (P=32) and the W-form integer loads (P=8/16/32
// by access width), with sharper value-derived thresholds -- in both W
// and X form -- for UBFM (P from the field geometry, covering
// LSR/UBFX/UXTB/UXTH), AND/ANDS immediate (P = top set bit of the mask
// + 1), MOVZ (P = bit count of the known value), and CSINC Rd, ZR, ZR
// (CSET: P = 1). Consumers: an in-place UBFM with immr=0 of any width
// (UXTB/UXTH/UXTW and general UBFX #0, #C), an AND with a contiguous
// low mask (C = mask width), or MOV Wd, Wd (C = 32 via the W write).
bool check_redundant_zext(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect a sign-extending producer (LDRSB / LDRSH / LDRSW, or any
// SBFM: the SXTB / SXTH / SXTW aliases, ASR immediate, and the
// general SBFX / SBFIZ shapes, whose sign threshold follows from the
// field geometry) immediately followed by an SXTB / SXTH / SXTW
// consumer whose destination width matches the producer's and whose
// sign threshold is at least the producer's. The consumer is
// redundant: the producer already replicated the sign bit through the
// same upper bits.
bool check_redundant_sext(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect LSL Rd, Rs, #a immediately followed by LSR/ASR Rd, Rd, #b
// with b >= a. The pair extracts bits Rs[datasize-a-1 .. b-a] and
// zero- or sign-extends them; equivalent to a single UBFX/SBFX
// Rd, Rs, #(b-a), #(datasize-b).
bool check_lsl_lsr_to_ubfx(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect LSR Rd, Rs, #n immediately followed by AND Rd, Rd, #((1<<w)-1)
// (any width 1..datasize-1). The pair extracts bits Rs[n+w-1 .. n] and
// zero-extends; equivalent to a single UBFX Rd, Rs, #n, #w (capping
// width at datasize-n if the mask is wider than the LSR-fillable bits).
bool check_lsr_and_to_ubfx(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Mirror of check_lsr_and_to_ubfx for the opposite ("mask then
// shift-right") order. Detect AND Rd, Rs, #mask -- where mask is a
// single contiguous run of 1s [lo, hi] -- immediately followed by
// LSR Rd, Rd, #n (the LSR reads and writes the AND's destination). The
// pair extracts Rs[hi .. n] (the run bits at or above the shift) into
// the low bits; equivalent to a single UBFX Rd, Rs, #n, #(hi+1-n).
//
// Foldable only when lo <= n <= hi: lo > n would leave the field above
// bit 0 (no single-UBFX form), and n > hi shifts the whole run out
// (a degenerate zero result). Replicated/rotated-wrapping masks are
// not single runs and are skipped. ANDS (flag-setting) is excluded.
bool check_and_lsr_to_ubfx(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// The left-shift mirror of the two checks above: an AND-low-mask or LSR
// immediately followed by LSL Rd, Rd, #n (the LSL reads and writes the
// producer's destination).
// and wd, ws, #((1<<w)-1) ; lsl wd, wd, #n -> ubfiz wd, ws, #n, #w
// ((ws & low-w-bits) << n; width capped at datasize-n when it would
// overflow), and
// lsr wd, ws, #a ; lsl wd, wd, #a -> and wd, ws, #~((1<<a)-1)
// (equal shifts clear the low a bits).
// LSR + LSL with unequal shifts has no single-instruction form -- the
// surviving field is neither low- nor zero-aligned -- and is not
// flagged. ANDS (flag-setting) is excluded by the low-mask decoder.
bool check_and_lsr_lsl_fold(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect MOV Xd, Xd encoded as ORR Xd, XZR, Xd, LSL #0 -- a literal
// no-op that reads and writes the same 64 bits. The W-form MOV Wd, Wd
// is NOT flagged here: it zero-extends X[63:32] and is handled as a
// consumer in check_redundant_zext.
bool check_mov_reg_self(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect ADD/SUB (immediate) with imm = 0 and the non-flag-setting
// variant (S = 0). When Rd == Rn the instruction is a no-op; when
// Rd != Rn it is equivalent to MOV Rd, Rn. The SP encoding (Rd = 31
// or Rn = 31) is excluded because that's the canonical MOV-to/from-SP
// alias. The Rd == Rn case immediately following an ADR/ADRP with
// the same Rd is also excluded: that's the linker-resolved
// "page-relative addressing" pair where the offset happened to be 0,
// removable only by re-linking.
// Detect a non-flag-setting ADD/SUB (immediate) immediately followed
// by a second one that reads its destination -- two adjustments of the
// same register by constants, which one instruction can carry:
//
// add x11, sp, #0x130 ; add x11, x11, #0x81 -> add x11, sp, #0x1b1
// sub x8, x29, #0x100 ; add x8, x8, #0x30 -> sub x8, x29, #0xd0
//
// ("ADD/SUB immediate chain foldable to one"). The kinds mix freely:
// each instruction contributes a signed amount and the fold renders
// whichever of ADD/SUB carries the sum, or MOV when they cancel.
//
// The sum must encode as ADD/SUB's 12-bit field, optionally shifted
// left by 12. That single gate also handles the compiler's own split
// of a wide constant (`add x8, x8, #0x1, lsl #12 ; add x8, x8, #0x20`)
// with no special case: the shifted form reaches only multiples of
// 4096, so a sum of 0x1020 fails and the already-minimal pair stays
// unflagged. Widths must agree -- a W-form producer zero-extends its
// 32-bit sum before an X-form consumer reads it, which 64-bit
// arithmetic on the original source does not reproduce.
//
// Both instructions must be non-flag-setting. An ADDS/SUBS producer
// cannot be deleted without losing its NZCV write; an ADDS/SUBS
// consumer is excluded for a subtler reason -- the folded instruction
// computes the same result but not the same flags, because C and V
// depend on the intermediate the fold erases.
//
// A zero adjustment on either side is not a chain but a redundant
// instruction, and check_add_sub_zero already reports it on its own
// terms; neither end opens or closes on one, so no window is reported
// twice.
//
// The rewrite deletes the producer, so its destination must be dead
// afterward: a consumer writing that same register kills it
// structurally (the dominant shape), and a fresh destination defers
// through the forward register-liveness scan. A producer writing SP
// (Rd = 31 in this encoding) never opens -- the stack pointer is never
// dead, since an asynchronous signal delivered between the two
// instructions observes the intermediate value.
//
// The shape is stack-address arithmetic: an object's frame offset is
// not a constant until the compiler assigns the frame layout, long
// after a field offset was fixed at instruction selection, so the two
// constants never meet a folding peephole. See TODO.md for the
// mechanism and the corpus measurements.
bool check_add_sub_imm_chain(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
bool check_add_sub_zero(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect self-op identities: AND/ORR Rd, Rs, Rs (shifted-register,
// LSL #0, Rn == Rm) collapses to MOV Rd, Rs; EOR/SUB/BIC Rd, Rs, Rs
// collapses to MOV Rd, XZR (zero); ORN/EON Rd, Rs, Rs collapses to
// MOV Rd, #-1 (MOVN Rd, #0). Flag-setting variants (ANDS/SUBS/BICS)
// are skipped because the flag-set is the user's intent.
bool check_self_op(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// The vector twin of check_self_op: an ASIMD three-same operation whose
// two source registers are the same, so the result is a constant or the
// operand back.
//
// eor v26.16b, v26.16b, v26.16b -> movi v26.2d, #0
// and v0.16b, v1.16b, v1.16b -> mov v0.16b, v1.16b
// orr v0.16b, v0.16b, v0.16b -> delete
//
// EOR, BIC and SUB collapse to zero; AND and ORR give the operand back.
// ORN gives all-ones, a different rewrite, and is absent from the
// mining corpus. BSL/BIT/BIF share EOR's U bit but read the
// destination as a third source, so they are a different shape.
//
// One member must NOT be flagged: `orr Vd, Vn, Vn` with Rd != Rn is the
// canonical spelling of the vector MOV, emitted for every
// `mov vd.16b, vn.16b`. Only its in-place form -- writing a register
// its own value -- is a finding. Counting the copies inflated a first
// pass over this shape from 353 to 2,158.
//
// Like the scalar check this is 1-for-1 with no liveness argument: the
// destination and its value are unchanged, nothing is deleted except in
// the in-place identity case, and no flags or memory are touched. What
// it buys is not size but issue: MOVI with a zero immediate is on
// Neoverse V2's "Zero Latency MOVs" list (section 4.12) and on Apple
// Firestorm's rename-eliminated set, while the self-op is on neither
// and occupies a vector pipe slot -- and, being in place at every site
// in the corpus, carries a false dependency on the register's own
// previous value. Neoverse N1's guide documents no such elimination, so
// this is "cheaper on newer cores, neutral on older", like
// check_and_lo32_mov.
bool check_vector_self_op(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// An ALU instruction whose Rm operand is the zero register, so the
// operation collapses to a copy, a constant, or a NOT:
//
// orr w0, w1, wzr -> mov w0, w1 add/sub/eor/bic likewise
// and w0, w1, wzr -> mov w0, wzr mul likewise
// orn w0, w1, wzr -> mov w0, #-1
// eon w0, w1, wzr -> mvn w0, w1
//
// Rm is the deliberate side. Every CANONICAL degenerate spelling puts
// ZR in Rn instead -- `mov Rd, Rm` is `orr Rd, ZR, Rm`, `neg` is
// `sub Rd, ZR, Rm`, `mvn` is `orn Rd, ZR, Rm` -- so requiring Rm = 31
// with Rn != 31 keeps all of them out without special-casing the alias
// table. Rd = 31 is excluded too: that instruction writes nothing and
// belongs to the dead-ZR-destination candidate, which the corpus
// measures at zero.
//
// The S-variants are excluded because their flag write is a second
// result the rewrite would drop, and the shifted forms because a
// shifted ZR is still zero but is not what the swept population
// counted. 1-for-1 with no liveness argument, like the two self-op
// checks: same destination, same value, no flags or memory touched.
bool check_zr_operand_alu(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect UMOV of lane 0 -- umov w0, v1.s[0], umov x0, v1.d[0], and
// under ARMLINT_FEATURE_FP16 umov w0, v1.h[0] -- which move exactly
// the bits FMOV Wd, Sn / Xd, Dn / Wd, Hn already reach, Sn/Dn/Hn being
// views of the low bits of Vn. One instruction either way; FMOV uses a
// cheaper port on Apple cores. Confined to lane 0 because FMOV
// (general) can only address the low element, and to the H/S/D sizes
// because no FMOV reads a byte element.
bool check_umov_lane0_fmov(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect the two X-form spellings that keep only the low 32 bits --
// AND Xd, Xn, #0xFFFFFFFF and UBFX Xd, Xn, #0, #32 -- both of which
// compute what MOV Wd, Wn already computes, every W write zeroing the
// upper half. One instruction either way; the MOV is a rename on
// Neoverse rather than an ALU op. Excludes an SP destination (AND-imm
// reads Rd = 31 as SP, the rewrite as WZR), a ZR destination (dead
// outright), a ZR source (a zero materialization, not a truncation),
// and Rd == Rn, whose rewrite would read "mov Wd, Wd" -- a real
// truncation that looks deletable; check_redundant_zext owns the
// in-place cases that really are deletable.
bool check_and_lo32_mov(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect CSEL Rd, Rn, Rn, cond -- the same-operand case where both
// branches of the conditional select produce Rn. The cond is
// irrelevant, the NZCV read is wasted, and the instruction is
// equivalent to MOV Rd, Rn. Only matches CSEL (op2 = 00); the other
// members of the family -- CSINC / CSINV / CSNEG -- have different
// "else" branches and are NOT identities when Rn == Rm.
bool check_csel_self(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// FP mirror of check_csel_self: FCSEL Vd, Vn, Vm, cond with Vn == Vm
// selects the same value on both branches, so the condition is
// irrelevant and the instruction is a register copy --
// FMOV (register). FCSEL is a pure bit-pattern select (no
// arithmetic, no NaN processing), and both FCSEL and FMOV zero the
// vector register above the written lane, so the rewrite is exact
// for the full 128 bits; the pointless NZCV read disappears too.
// Single and double precision only (half precision, FEAT_FP16, is
// not matched, consistent with the other FP checks); FP registers
// have no ZR/SP encoding, so no operand exclusions apply. Reported
// as "FCSEL same-operand identity".
bool check_fcsel_self(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect the 3-instruction BFXIL synthesis pattern:
// AND Rd, Rd, #~mask ; clear Rd[w-1..0]
// AND Rt, Rs, #mask ; isolate Rs[w-1..0] into Rt
// ORR Rd, Rd, Rt ; combine
// (the two ANDs in either order). Equivalent to a single
// BFXIL Rd, Rs, #0, #w. Aliasing constraints: Rt != Rd (so the
// isolate doesn't clobber the cleared Rd in place), Rt != Rs (so
// the isolate doesn't modify the source), and Rs != Rd (the
// degenerate case where the source is the just-cleared register
// yields a no-op instead of BFXIL).
//
// The BFXIL/BFI rewrite writes only Rd and drops the isolate's temp Rt
// (it is never written by the rewrite). Emission is therefore deferred
// until the forward register-liveness scan (armlint_advance_pending_mz)
// proves Rt dead after the ORR; a downstream read of Rt before it is
// overwritten would make dropping the isolate unsound.
bool check_bfxil_synth(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect MUL Rd, Rn, Rm (the MADD Rd, Rn, Rm, ZR alias) where one
// operand is set by an immediately preceding MOV chain (MOVZ/MOVN +
// optional MOVKs) to a constant C. The MUL is foldable to a single
// shifted-register instruction when C is a small step from a power
// of two:
// C = 2^N (N >= 1) -> LSL Rd, R<other>, #N
// C = 2^N + 1 (N >= 1) -> ADD Rd, R<other>, R<other>, LSL #N
// The 2^N - 1 case is intentionally not folded: AArch64 has no single
// shifted-register form computing x*(2^N - 1) directly --
// SUB Xn, Xn, Xn, LSL #N gives x*(1 - 2^N), the negation -- so the
// rewrite needs two instructions (LSL+SUB or SUB+NEG), at parity with
// MOV+MUL in instruction count. The MOV chain's width (W vs X) must
// match the MUL's; MUL is commutative, so either Rn or Rm may be the
// MOV destination. ZR as Rd or as the "other" operand is excluded, as
// is the surviving operand being the MOV destination itself
// (MUL Rd, Xc, Xc): the rewrite would still read the constant
// register, so the MOV could never be deleted.
//
// Runs before check_movz_movk_bitmask in the registry: that check
// closes the MOV chain on any non-MOV instruction, so ours must
// inspect state->mov_active first.
bool check_mul_strength_reduce(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect MNEG Rd, Rn, Rm (the MSUB Rd, Rn, Rm, ZR alias) where one
// operand is set by an immediately preceding MOV chain to a constant
// C. The MNEG is foldable to a single shifted-register instruction
// for three families of constants:
// C = 1 -> NEG Rd, R<other>
// C = 2^N (N >= 1) -> NEG Rd, R<other>, LSL #N
// C = 2^N - 1 (N >= 2) -> SUB Rd, R<other>, R<other>, LSL #N
// The 2^N - 1 family is the elegant complement of MUL's 2^N + 1:
// SUB Xd, Xn, Xn, LSL #N computes x*(1 - 2^N) = -x*(2^N - 1), exactly
// what MNEG needs.
//
// 2^N + 1 (N >= 1) is NOT folded: the rewrite -((x << N) + x) is two
// instructions (ADD-shifted then NEG, or SUB+NEG), at parity with
// MOV+MNEG.
//
// Same plumbing as check_mul_strength_reduce -- runs before
// check_movz_movk_bitmask so the MOV chain state is still active.
// Shares its exclusions, including the surviving operand being the
// MOV destination itself (MNEG Rd, Xc, Xc).
bool check_mneg_strength_reduce(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect UDIV Rd, Rn, Rm where Rm is set by an immediately preceding
// MOV chain to a constant C that is a power of two (C = 2^N, N >= 1).
// The pair folds to a single shift:
// mov xc, #2^N ; udiv xd, xn, xc -> lsr xd, xn, #N
// Width (W vs X) of the MOV chain must match the UDIV.
//
// UDIV is NOT commutative, so unlike the MUL strength reduction only
// the divisor (Rm) can come from the MOV; an Rn-from-MOV match would
// be a reciprocal-multiply problem, not a shift. Non-pow2 divisors
// have no single-instruction shift rewrite and are excluded.
//
// SDIV is intentionally NOT included: SDIV by 2^N is not equivalent
// to ASR by N on negative dividends (SDIV rounds toward zero; ASR
// rounds toward -inf), so the fold would be incorrect.
//
// C == 0 is degenerate (UDIV by zero produces 0 on AArch64, no trap)
// and C == 1 is the identity case; both are excluded. Rd == ZR
// discards the result and Rn == ZR makes the dividend always zero --
// different idioms, not strength reduction. The dividend must also
// not be the MOV destination itself (UDIV Rd, Xc, Xc): the LSR
// rewrite would still read the constant register, so the MOV could
// never be deleted.
//
// Runs alongside check_mul_strength_reduce / check_mneg_strength_reduce
// before check_movz_movk_bitmask so the MOV chain state is still
// active.
bool check_udiv_strength_reduce(armlint_state *state, const cs_insn *insn,
size_t offset, armlint_finding *out);
// Detect ADD/ADDS/SUB/SUBS (shifted-register, LSL #0) where one
// operand is set by an immediately preceding MOV chain to a constant
// C that fits the AArch64 ADD/SUB immediate form (12-bit imm with
// optional LSL #12: C in [1, 0xFFF] or C a multiple of 0x1000 with
// C/0x1000 in [1, 0xFFF]). The pair folds to a single immediate-form
// instruction:
// mov xc, #C ; add xd, xn, xc -> add xd, xn, #C
// mov xc, #C ; adds xd, xn, xc -> adds xd, xn, #C
// mov xc, #C ; sub xd, xn, xc -> sub xd, xn, #C
// mov xc, #C ; subs xd, xn, xc -> subs xd, xn, #C