freedreno/afuc: Split out instruction decode helper

Split the giant switch/decode out into a helper function so that we can
re-use it for emulator mode.

Signed-off-by: Rob Clark <robdclark@chromium.org>
Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/10944>
This commit is contained in:
Rob Clark 2021-05-16 12:15:31 -07:00 committed by Marge Bot
parent 33b9445a68
commit d367d84d87
1 changed files with 407 additions and 401 deletions

View File

@ -300,6 +300,412 @@ print_control_reg(uint32_t id)
}
}
static void
disasm_instr(uint32_t *instrs, unsigned pc)
{
int jump_label_idx;
afuc_instr *instr = (void *)&instrs[pc];
const char *fname, *lname;
afuc_opc opc;
bool rep;
afuc_get_opc(instr, &opc, &rep);
lname = label_name(pc, false);
fname = fxn_name(pc);
jump_label_idx = get_jump_table_entry(pc);
if (jump_label_idx >= 0) {
int j;
printf("\n");
for (j = 0; j < jump_labels[jump_label_idx].num_jump_labels; j++) {
uint32_t jump_label = jump_labels[jump_label_idx].jump_labels[j];
const char *name = getpm4(jump_label);
if (name) {
printlbl("%s", name);
} else {
printlbl("UNKN%d", jump_label);
}
printf(":\n");
}
}
if (fname) {
printlbl("%s", fname);
printf(":\n");
}
if (lname) {
printlbl(" %s", lname);
printf(":");
} else {
printf(" ");
}
if (verbose) {
printf("\t%04x: %08x ", pc, instrs[pc]);
} else {
printf(" ");
}
switch (opc) {
case OPC_NOP: {
/* a6xx changed the default immediate, and apparently 0
* is illegal now.
*/
const uint32_t nop = gpuver >= 6 ? 0x1000000 : 0x0;
if (instrs[pc] != nop) {
printerr("[%08x]", instrs[pc]);
printf(" ; ");
}
if (rep)
printf("(rep)");
printf("nop");
print_gpu_reg(instrs[pc]);
break;
}
case OPC_ADD:
case OPC_ADDHI:
case OPC_SUB:
case OPC_SUBHI:
case OPC_AND:
case OPC_OR:
case OPC_XOR:
case OPC_NOT:
case OPC_SHL:
case OPC_USHR:
case OPC_ISHR:
case OPC_ROT:
case OPC_MUL8:
case OPC_MIN:
case OPC_MAX:
case OPC_CMP: {
bool src1 = true;
if (opc == OPC_NOT)
src1 = false;
if (rep)
printf("(rep)");
print_alu_name(opc, instrs[pc]);
print_dst(instr->alui.dst);
printf(", ");
if (src1) {
print_src(instr->alui.src);
printf(", ");
}
printf("0x%04x", instr->alui.uimm);
print_gpu_reg(instr->alui.uimm);
/* print out unexpected bits: */
if (verbose) {
if (instr->alui.src && !src1)
printerr(" (src=%02x)", instr->alui.src);
}
break;
}
case OPC_MOVI: {
if (rep)
printf("(rep)");
printf("mov ");
print_dst(instr->movi.dst);
printf(", 0x%04x", instr->movi.uimm);
if (instr->movi.shift)
printf(" << %u", instr->movi.shift);
/* using mov w/ << 16 is popular way to construct a pkt7
* header to send (for ex, from PFP to ME), so check that
* case first
*/
if ((instr->movi.shift == 16) &&
((instr->movi.uimm & 0xff00) == 0x7000)) {
unsigned opc, p;
opc = instr->movi.uimm & 0x7f;
p = pm4_odd_parity_bit(opc);
/* So, you'd think that checking the parity bit would be
* a good way to rule out false positives, but seems like
* ME doesn't really care.. at least it would filter out
* things that look like actual legit packets between
* PFP and ME..
*/
if (1 || p == ((instr->movi.uimm >> 7) & 0x1)) {
const char *name = getpm4(opc);
printf("\t; ");
if (name)
printlbl("%s", name);
else
printlbl("UNKN%u", opc);
break;
}
}
print_gpu_reg(instr->movi.uimm << instr->movi.shift);
break;
}
case OPC_ALU: {
bool src1 = true;
if (instr->alu.alu == OPC_NOT || instr->alu.alu == OPC_MSB)
src1 = false;
if (instr->alu.pad)
printf("[%08x] ; ", instrs[pc]);
if (rep)
printf("(rep)");
if (instr->alu.xmov)
printf("(xmov%d)", instr->alu.xmov);
/* special case mnemonics:
* reading $00 seems to always yield zero, and so:
* or $dst, $00, $src -> mov $dst, $src
* Maybe add one for negate too, ie.
* sub $dst, $00, $src ???
*/
if ((instr->alu.alu == OPC_OR) && !instr->alu.src1) {
printf("mov ");
src1 = false;
} else {
print_alu_name(instr->alu.alu, instrs[pc]);
}
print_dst(instr->alu.dst);
if (src1) {
printf(", ");
print_src(instr->alu.src1);
}
printf(", ");
print_src(instr->alu.src2);
/* print out unexpected bits: */
if (verbose) {
if (instr->alu.pad)
printerr(" (pad=%01x)", instr->alu.pad);
if (instr->alu.src1 && !src1)
printerr(" (src1=%02x)", instr->alu.src1);
}
/* xmov is a modifier that makes the processor execute up to 3
* extra mov's after the current instruction. Given an ALU
* instruction:
*
* (xmovN) alu $dst, $src1, $src2
*
* In all of the uses in the firmware blob, $dst and $src2 are one
* of the "special" registers $data, $addr, $addr2. I've observed
* that if $dst isn't "special" then it's replaced with $00
* instead of $data, but I haven't checked what happens if $src2
* isn't "special". Anyway, in the usual case, the HW produces a
* count M = min(N, $rem) and then does the following:
*
* M = 1:
* mov $data, $src2
*
* M = 2:
* mov $data, $src2
* mov $data, $src2
*
* M = 3:
* mov $data, $src2
* mov $dst, $src2 (special case for CP_CONTEXT_REG_BUNCH)
* mov $data, $src2
*
* It seems to be frequently used in combination with (rep) to
* provide a kind of hardware-based loop unrolling, and there's
* even a special case in the ISA to be able to do this with
* CP_CONTEXT_REG_BUNCH. However (rep) isn't required.
*
* This dumps the expected extra instructions, assuming that $rem
* isn't too small.
*/
if (verbose && instr->alu.xmov) {
for (int i = 0; i < instr->alu.xmov; i++) {
printf("\n ; mov ");
if (instr->alu.dst < 0x1d)
printf("$00");
else if (instr->alu.xmov == 3 && i == 1)
print_dst(instr->alu.dst);
else
printf("$data");
printf(", ");
print_src(instr->alu.src2);
}
}
break;
}
case OPC_CWRITE6:
case OPC_CREAD6:
case OPC_STORE6:
case OPC_LOAD6: {
if (rep)
printf("(rep)");
bool is_control_reg = true;
if (gpuver >= 6) {
switch (opc) {
case OPC_CWRITE6:
printf("cwrite ");
break;
case OPC_CREAD6:
printf("cread ");
break;
case OPC_STORE6:
is_control_reg = false;
printf("store ");
break;
case OPC_LOAD6:
is_control_reg = false;
printf("load ");
break;
default:
assert(!"unreachable");
}
} else {
switch (opc) {
case OPC_CWRITE5:
printf("cwrite ");
break;
case OPC_CREAD5:
printf("cread ");
break;
default:
fprintf(stderr, "A6xx control opcode on A5xx?\n");
exit(1);
}
}
print_src(instr->control.src1);
printf(", [");
print_src(instr->control.src2);
printf(" + ");
if (is_control_reg && instr->control.flags != 0x4)
print_control_reg(instr->control.uimm);
else
printf("0x%03x", instr->control.uimm);
printf("], 0x%x", instr->control.flags);
break;
}
case OPC_BRNEI:
case OPC_BREQI:
case OPC_BRNEB:
case OPC_BREQB: {
unsigned off = pc + instr->br.ioff;
assert(!rep);
/* Since $00 reads back zero, it can be used as src for
* unconditional branches. (This only really makes sense
* for the BREQB.. or possible BRNEI if imm==0.)
*
* If bit=0 then branch is taken if *all* bits are zero.
* Otherwise it is taken if bit (bit-1) is clear.
*
* Note the instruction after a jump/branch is executed
* regardless of whether branch is taken, so use nop or
* take that into account in code.
*/
if (instr->br.src || (opc != OPC_BRNEB)) {
bool immed = false;
if (opc == OPC_BRNEI) {
printf("brne ");
immed = true;
} else if (opc == OPC_BREQI) {
printf("breq ");
immed = true;
} else if (opc == OPC_BRNEB) {
printf("brne ");
} else if (opc == OPC_BREQB) {
printf("breq ");
}
print_src(instr->br.src);
if (immed) {
printf(", 0x%x,", instr->br.bit_or_imm);
} else {
printf(", b%u,", instr->br.bit_or_imm);
}
} else {
printf("jump");
if (verbose && instr->br.bit_or_imm) {
printerr(" (src=%03x, bit=%03x) ", instr->br.src,
instr->br.bit_or_imm);
}
}
printf(" #");
printlbl("%s", label_name(off, true));
if (verbose)
printf(" (#%d, %04x)", instr->br.ioff, off);
break;
}
case OPC_CALL:
assert(!rep);
printf("call #");
printlbl("%s", fxn_name(instr->call.uoff));
if (verbose) {
printf(" (%04x)", instr->call.uoff);
if (instr->br.bit_or_imm || instr->br.src) {
printerr(" (src=%03x, bit=%03x) ", instr->br.src,
instr->br.bit_or_imm);
}
}
break;
case OPC_RET:
assert(!rep);
if (instr->ret.pad)
printf("[%08x] ; ", instrs[pc]);
if (instr->ret.interrupt)
printf("iret");
else
printf("ret");
break;
case OPC_WIN:
assert(!rep);
if (instr->waitin.pad)
printf("[%08x] ; ", instrs[pc]);
printf("waitin");
if (verbose && instr->waitin.pad)
printerr(" (pad=%x)", instr->waitin.pad);
break;
case OPC_PREEMPTLEAVE6:
if (gpuver < 6) {
printf("[%08x] ; op38", instrs[pc]);
} else {
printf("preemptleave #");
printlbl("%s", label_name(instr->call.uoff, true));
}
break;
case OPC_SETSECURE:
/* Note: This seems to implicitly read the secure/not-secure state
* to set from the low bit of $02, and implicitly jumps to pc + 3
* (i.e. skipping the next two instructions) if it succeeds. We
* print these implicit parameters to make reading the disassembly
* easier.
*/
if (instr->pad)
printf("[%08x] ; ", instrs[pc]);
printf("setsecure $02, #");
printlbl("%s", label_name(pc + 3, true));
break;
default:
printerr("[%08x]", instrs[pc]);
printf(" ; op%02x ", opc);
print_dst(instr->alui.dst);
printf(", ");
print_src(instr->alui.src);
print_gpu_reg(instrs[pc] & 0xffff);
break;
}
printf("\n");
}
static void
disasm(uint32_t *buf, int sizedwords)
{
@ -350,407 +756,7 @@ disasm(uint32_t *buf, int sizedwords)
/* print instructions: */
for (i = 0; i < jmptbl_start; i++) {
int jump_label_idx;
afuc_instr *instr = (void *)&instrs[i];
const char *fname, *lname;
afuc_opc opc;
bool rep;
afuc_get_opc(instr, &opc, &rep);
lname = label_name(i, false);
fname = fxn_name(i);
jump_label_idx = get_jump_table_entry(i);
if (jump_label_idx >= 0) {
int j;
printf("\n");
for (j = 0; j < jump_labels[jump_label_idx].num_jump_labels; j++) {
uint32_t jump_label = jump_labels[jump_label_idx].jump_labels[j];
const char *name = getpm4(jump_label);
if (name) {
printlbl("%s", name);
} else {
printlbl("UNKN%d", jump_label);
}
printf(":\n");
}
}
if (fname) {
printlbl("%s", fname);
printf(":\n");
}
if (lname) {
printlbl(" %s", lname);
printf(":");
} else {
printf(" ");
}
if (verbose) {
printf("\t%04x: %08x ", i, instrs[i]);
} else {
printf(" ");
}
switch (opc) {
case OPC_NOP: {
/* a6xx changed the default immediate, and apparently 0
* is illegal now.
*/
const uint32_t nop = gpuver >= 6 ? 0x1000000 : 0x0;
if (instrs[i] != nop) {
printerr("[%08x]", instrs[i]);
printf(" ; ");
}
if (rep)
printf("(rep)");
printf("nop");
print_gpu_reg(instrs[i]);
break;
}
case OPC_ADD:
case OPC_ADDHI:
case OPC_SUB:
case OPC_SUBHI:
case OPC_AND:
case OPC_OR:
case OPC_XOR:
case OPC_NOT:
case OPC_SHL:
case OPC_USHR:
case OPC_ISHR:
case OPC_ROT:
case OPC_MUL8:
case OPC_MIN:
case OPC_MAX:
case OPC_CMP: {
bool src1 = true;
if (opc == OPC_NOT)
src1 = false;
if (rep)
printf("(rep)");
print_alu_name(opc, instrs[i]);
print_dst(instr->alui.dst);
printf(", ");
if (src1) {
print_src(instr->alui.src);
printf(", ");
}
printf("0x%04x", instr->alui.uimm);
print_gpu_reg(instr->alui.uimm);
/* print out unexpected bits: */
if (verbose) {
if (instr->alui.src && !src1)
printerr(" (src=%02x)", instr->alui.src);
}
break;
}
case OPC_MOVI: {
if (rep)
printf("(rep)");
printf("mov ");
print_dst(instr->movi.dst);
printf(", 0x%04x", instr->movi.uimm);
if (instr->movi.shift)
printf(" << %u", instr->movi.shift);
/* using mov w/ << 16 is popular way to construct a pkt7
* header to send (for ex, from PFP to ME), so check that
* case first
*/
if ((instr->movi.shift == 16) &&
((instr->movi.uimm & 0xff00) == 0x7000)) {
unsigned opc, p;
opc = instr->movi.uimm & 0x7f;
p = pm4_odd_parity_bit(opc);
/* So, you'd think that checking the parity bit would be
* a good way to rule out false positives, but seems like
* ME doesn't really care.. at least it would filter out
* things that look like actual legit packets between
* PFP and ME..
*/
if (1 || p == ((instr->movi.uimm >> 7) & 0x1)) {
const char *name = getpm4(opc);
printf("\t; ");
if (name)
printlbl("%s", name);
else
printlbl("UNKN%u", opc);
break;
}
}
print_gpu_reg(instr->movi.uimm << instr->movi.shift);
break;
}
case OPC_ALU: {
bool src1 = true;
if (instr->alu.alu == OPC_NOT || instr->alu.alu == OPC_MSB)
src1 = false;
if (instr->alu.pad)
printf("[%08x] ; ", instrs[i]);
if (rep)
printf("(rep)");
if (instr->alu.xmov)
printf("(xmov%d)", instr->alu.xmov);
/* special case mnemonics:
* reading $00 seems to always yield zero, and so:
* or $dst, $00, $src -> mov $dst, $src
* Maybe add one for negate too, ie.
* sub $dst, $00, $src ???
*/
if ((instr->alu.alu == OPC_OR) && !instr->alu.src1) {
printf("mov ");
src1 = false;
} else {
print_alu_name(instr->alu.alu, instrs[i]);
}
print_dst(instr->alu.dst);
if (src1) {
printf(", ");
print_src(instr->alu.src1);
}
printf(", ");
print_src(instr->alu.src2);
/* print out unexpected bits: */
if (verbose) {
if (instr->alu.pad)
printerr(" (pad=%01x)", instr->alu.pad);
if (instr->alu.src1 && !src1)
printerr(" (src1=%02x)", instr->alu.src1);
}
/* xmov is a modifier that makes the processor execute up to 3
* extra mov's after the current instruction. Given an ALU
* instruction:
*
* (xmovN) alu $dst, $src1, $src2
*
* In all of the uses in the firmware blob, $dst and $src2 are one
* of the "special" registers $data, $addr, $addr2. I've observed
* that if $dst isn't "special" then it's replaced with $00
* instead of $data, but I haven't checked what happens if $src2
* isn't "special". Anyway, in the usual case, the HW produces a
* count M = min(N, $rem) and then does the following:
*
* M = 1:
* mov $data, $src2
*
* M = 2:
* mov $data, $src2
* mov $data, $src2
*
* M = 3:
* mov $data, $src2
* mov $dst, $src2 (special case for CP_CONTEXT_REG_BUNCH)
* mov $data, $src2
*
* It seems to be frequently used in combination with (rep) to
* provide a kind of hardware-based loop unrolling, and there's
* even a special case in the ISA to be able to do this with
* CP_CONTEXT_REG_BUNCH. However (rep) isn't required.
*
* This dumps the expected extra instructions, assuming that $rem
* isn't too small.
*/
if (verbose && instr->alu.xmov) {
for (int i = 0; i < instr->alu.xmov; i++) {
printf("\n ; mov ");
if (instr->alu.dst < 0x1d)
printf("$00");
else if (instr->alu.xmov == 3 && i == 1)
print_dst(instr->alu.dst);
else
printf("$data");
printf(", ");
print_src(instr->alu.src2);
}
}
break;
}
case OPC_CWRITE6:
case OPC_CREAD6:
case OPC_STORE6:
case OPC_LOAD6: {
if (rep)
printf("(rep)");
bool is_control_reg = true;
if (gpuver >= 6) {
switch (opc) {
case OPC_CWRITE6:
printf("cwrite ");
break;
case OPC_CREAD6:
printf("cread ");
break;
case OPC_STORE6:
is_control_reg = false;
printf("store ");
break;
case OPC_LOAD6:
is_control_reg = false;
printf("load ");
break;
default:
assert(!"unreachable");
}
} else {
switch (opc) {
case OPC_CWRITE5:
printf("cwrite ");
break;
case OPC_CREAD5:
printf("cread ");
break;
default:
fprintf(stderr, "A6xx control opcode on A5xx?\n");
exit(1);
}
}
print_src(instr->control.src1);
printf(", [");
print_src(instr->control.src2);
printf(" + ");
if (is_control_reg && instr->control.flags != 0x4)
print_control_reg(instr->control.uimm);
else
printf("0x%03x", instr->control.uimm);
printf("], 0x%x", instr->control.flags);
break;
}
case OPC_BRNEI:
case OPC_BREQI:
case OPC_BRNEB:
case OPC_BREQB: {
unsigned off = i + instr->br.ioff;
assert(!rep);
/* Since $00 reads back zero, it can be used as src for
* unconditional branches. (This only really makes sense
* for the BREQB.. or possible BRNEI if imm==0.)
*
* If bit=0 then branch is taken if *all* bits are zero.
* Otherwise it is taken if bit (bit-1) is clear.
*
* Note the instruction after a jump/branch is executed
* regardless of whether branch is taken, so use nop or
* take that into account in code.
*/
if (instr->br.src || (opc != OPC_BRNEB)) {
bool immed = false;
if (opc == OPC_BRNEI) {
printf("brne ");
immed = true;
} else if (opc == OPC_BREQI) {
printf("breq ");
immed = true;
} else if (opc == OPC_BRNEB) {
printf("brne ");
} else if (opc == OPC_BREQB) {
printf("breq ");
}
print_src(instr->br.src);
if (immed) {
printf(", 0x%x,", instr->br.bit_or_imm);
} else {
printf(", b%u,", instr->br.bit_or_imm);
}
} else {
printf("jump");
if (verbose && instr->br.bit_or_imm) {
printerr(" (src=%03x, bit=%03x) ", instr->br.src,
instr->br.bit_or_imm);
}
}
printf(" #");
printlbl("%s", label_name(off, true));
if (verbose)
printf(" (#%d, %04x)", instr->br.ioff, off);
break;
}
case OPC_CALL:
assert(!rep);
printf("call #");
printlbl("%s", fxn_name(instr->call.uoff));
if (verbose) {
printf(" (%04x)", instr->call.uoff);
if (instr->br.bit_or_imm || instr->br.src) {
printerr(" (src=%03x, bit=%03x) ", instr->br.src,
instr->br.bit_or_imm);
}
}
break;
case OPC_RET:
assert(!rep);
if (instr->ret.pad)
printf("[%08x] ; ", instrs[i]);
if (instr->ret.interrupt)
printf("iret");
else
printf("ret");
break;
case OPC_WIN:
assert(!rep);
if (instr->waitin.pad)
printf("[%08x] ; ", instrs[i]);
printf("waitin");
if (verbose && instr->waitin.pad)
printerr(" (pad=%x)", instr->waitin.pad);
break;
case OPC_PREEMPTLEAVE6:
if (gpuver < 6) {
printf("[%08x] ; op38", instrs[i]);
} else {
printf("preemptleave #");
printlbl("%s", label_name(instr->call.uoff, true));
}
break;
case OPC_SETSECURE:
/* Note: This seems to implicitly read the secure/not-secure state
* to set from the low bit of $02, and implicitly jumps to pc + 3
* (i.e. skipping the next two instructions) if it succeeds. We
* print these implicit parameters to make reading the disassembly
* easier.
*/
if (instr->pad)
printf("[%08x] ; ", instrs[i]);
printf("setsecure $02, #");
printlbl("%s", label_name(i + 3, true));
break;
default:
printerr("[%08x]", instrs[i]);
printf(" ; op%02x ", opc);
print_dst(instr->alui.dst);
printf(", ");
print_src(instr->alui.src);
print_gpu_reg(instrs[i] & 0xffff);
break;
}
printf("\n");
disasm_instr(instrs, i);
}
/* print jumptable: */