mirror of
https://github.com/odin-lang/Odin.git
synced 2026-07-31 19:28:55 +00:00
Merge pull request #7168 from odin-lang/bill/rexcode
rexcode/x86: label addressing for RIP-relative disp and movabs imm
This commit is contained in:
@@ -122,6 +122,7 @@ Memory :: bit_field u64 {
|
||||
addr_size_override: bool | 1,
|
||||
base_class: u8 | 5,
|
||||
index_class: u8 | 5,
|
||||
disp_is_label: bool | 1, // disp holds a label id -> REL32 relocation
|
||||
}
|
||||
MEM_BASE_RIP :: 30 MEM_BASE_NONE :: 31 MEM_INDEX_NONE :: 31
|
||||
```
|
||||
@@ -131,7 +132,17 @@ MEM_BASE_RIP :: 30 MEM_BASE_NONE :: 31 MEM_INDEX_NONE :: 31
|
||||
**Convenience constructors** (current names after the in-tree refactor):
|
||||
`mem_base_only(base)`, `mem_base_disp(base, disp)`,
|
||||
`mem_base_index(base, index, scale)`,
|
||||
`mem_base_index_disp(base, index, scale, disp)`, `mem_rip_disp(disp)`.
|
||||
`mem_base_index_disp(base, index, scale, disp)`, `mem_rip_disp(disp)`,
|
||||
`mem_rip_label(label_id)`.
|
||||
|
||||
> **Label addressing.** `mem_rip_label(label_id)` builds a RIP-relative operand
|
||||
> whose displacement references a **label** rather than a literal: the encoder
|
||||
> writes a placeholder disp32 and emits a `REL32` relocation for `label_id` at
|
||||
> that field (addend 0), exactly as the `.RELATIVE` jump/call path does. This is
|
||||
> how `lea reg, [rip + <label>]` (position-independent data addressing) is
|
||||
> expressed. When the label is defined the disp resolves in place; otherwise the
|
||||
> relocation is appended to the caller's `relocs`. The `disp_is_label` bit reuses
|
||||
> one of `Memory`'s spare bits — no struct growth.
|
||||
|
||||
> ⚠️ `mem_base` is an **accessor** (returns the base `Register`), not a
|
||||
> constructor — use `mem_base_only` for the no-displacement case.
|
||||
@@ -158,17 +169,24 @@ Operand :: struct #packed { // 16 bytes
|
||||
Broadcast :: enum u8 { NONE, B1TO2, B1TO4, B1TO8, B1TO16 } // EVEX
|
||||
|
||||
Operand_Flags :: bit_field u16 { // EVEX-specific
|
||||
mask: u8 | 3, // opmask K1–K7
|
||||
zeroing: bool | 1, // merge vs zero masking
|
||||
broadcast: Broadcast | 3,
|
||||
er_sae: u8 | 2, // embedded rounding / SAE
|
||||
mask: u8 | 3, // opmask K1–K7
|
||||
zeroing: bool | 1, // merge vs zero masking
|
||||
broadcast: Broadcast | 3,
|
||||
er_sae: u8 | 2, // embedded rounding / SAE
|
||||
imm_is_label: bool | 1, // movabs imm64 holds a label id -> ABS64 reloc
|
||||
}
|
||||
```
|
||||
|
||||
### Generic operand constructors
|
||||
|
||||
`op_reg(r)`, `op_mem(m, size)`, `op_mem_from_parts(base, index, scale, disp, size)`,
|
||||
`op_imm8/16/32/64(v)`, `op_rel8/32(offset)`, `op_label(label_id, size=4)`.
|
||||
`op_imm8/16/32/64(v)`, `op_rel8/32(offset)`, `op_label(label_id, size=4)`,
|
||||
`op_imm_label(label_id)`.
|
||||
|
||||
> `op_imm_label(label_id)` is a `movabs reg, <label>` immediate: it stays kind
|
||||
> `.IMMEDIATE` (form matching unchanged) but is flagged so the encoder emits an
|
||||
> `ABS64` relocation (size 8) for the imm64 instead of a literal. The label
|
||||
> always selects the full imm64 form regardless of the id's numeric value.
|
||||
|
||||
### Typed operand constructors (compile-time class safety)
|
||||
|
||||
|
||||
@@ -178,8 +178,14 @@ encode :: proc(
|
||||
if form_index >= 0 && form_index < len(ENCODE_RECIPES) {
|
||||
recipe := &ENCODE_RECIPES[form_index]
|
||||
if recipe.flags.eligible && transmute(u8)inst.flags == 0 {
|
||||
imm_lit := recipe.imm_op < 0 || inst.ops[recipe.imm_op].kind == .IMMEDIATE
|
||||
if imm_lit {
|
||||
// The recipe emitter is contextless and cannot append relocations,
|
||||
// so a labeled immediate or a labeled memory displacement must fall
|
||||
// back to the interpreter (which emits the relocation).
|
||||
imm_lit := recipe.imm_op < 0 ||
|
||||
(inst.ops[recipe.imm_op].kind == .IMMEDIATE && !inst.ops[recipe.imm_op].flags.imm_is_label)
|
||||
rm_lit := recipe.rm_op < 0 ||
|
||||
inst.ops[recipe.rm_op].kind != .MEMORY || !inst.ops[recipe.rm_op].mem.disp_is_label
|
||||
if imm_lit && rm_lit {
|
||||
byte_count += emit_recipe(recipe, &inst, code[byte_count:])
|
||||
continue
|
||||
}
|
||||
@@ -545,6 +551,8 @@ encode :: proc(
|
||||
sib: u8 = 0
|
||||
disp: i32 = 0
|
||||
displacement_size: u8 = 0
|
||||
disp_is_label := false // disp32 references a label -> emit REL32
|
||||
disp_label_id: u32 = 0
|
||||
|
||||
// Reg field
|
||||
if enc.flags.modrm_reg_ext {
|
||||
@@ -572,6 +580,11 @@ encode :: proc(
|
||||
rm = 0b101
|
||||
disp = m.disp
|
||||
displacement_size = 4
|
||||
// A RIP-relative disp can carry a label id instead of a
|
||||
// literal: emit a REL32 relocation at the disp32 offset
|
||||
// (mirrors the .RELATIVE -> REL32 jump/call path).
|
||||
disp_is_label = m.disp_is_label
|
||||
disp_label_id = u32(m.disp)
|
||||
} else if !mem_has_base(m) && !mem_has_index(m) {
|
||||
mod = 0b00
|
||||
rm = 0b100
|
||||
@@ -633,13 +646,21 @@ encode :: proc(
|
||||
pos += 1
|
||||
}
|
||||
|
||||
// Displacement: bounded little-endian emit. Kept as a counted loop
|
||||
// (0/1/4 trips, highly predictable per code pattern) so no buffer
|
||||
// tail-slack is needed and no bytes are written past the real size.
|
||||
for _ in 0..<displacement_size {
|
||||
out[pos] = u8(disp & 0xFF)
|
||||
disp >>= 8
|
||||
pos += 1
|
||||
// Displacement. A labeled disp emits a placeholder disp32 and records a
|
||||
// REL32 relocation at its byte offset (addend 0). Otherwise a bounded
|
||||
// little-endian emit -- kept as a counted loop (0/1/4 trips, highly
|
||||
// predictable per code pattern) so no buffer tail-slack is needed and no
|
||||
// bytes are written past the real size.
|
||||
if disp_is_label {
|
||||
append(&pending_relocations, Relocation{byte_count + pos, disp_label_id, 0, .REL32, 4, u16(instruction_index)})
|
||||
out[pos] = 0; out[pos+1] = 0; out[pos+2] = 0; out[pos+3] = 0
|
||||
pos += 4
|
||||
} else {
|
||||
for _ in 0..<displacement_size {
|
||||
out[pos] = u8(disp & 0xFF)
|
||||
disp >>= 8
|
||||
pos += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -693,10 +714,19 @@ encode :: proc(
|
||||
}
|
||||
case .IQ:
|
||||
if user_op.kind == .IMMEDIATE {
|
||||
v := u64(user_op.immediate)
|
||||
out[pos] = u8(v); out[pos+1] = u8(v >> 8); out[pos+2] = u8(v >> 16); out[pos+3] = u8(v >> 24)
|
||||
out[pos+4] = u8(v >> 32); out[pos+5] = u8(v >> 40); out[pos+6] = u8(v >> 48); out[pos+7] = u8(v >> 56)
|
||||
pos += 8
|
||||
if user_op.flags.imm_is_label {
|
||||
// movabs reg, <label>: placeholder imm64 + ABS64 relocation.
|
||||
label_id := u32(user_op.immediate)
|
||||
append(&pending_relocations, Relocation{byte_count + pos, label_id, 0, .ABS64, 8, u16(instruction_index)})
|
||||
out[pos] = 0; out[pos+1] = 0; out[pos+2] = 0; out[pos+3] = 0
|
||||
out[pos+4] = 0; out[pos+5] = 0; out[pos+6] = 0; out[pos+7] = 0
|
||||
pos += 8
|
||||
} else {
|
||||
v := u64(user_op.immediate)
|
||||
out[pos] = u8(v); out[pos+1] = u8(v >> 8); out[pos+2] = u8(v >> 16); out[pos+3] = u8(v >> 24)
|
||||
out[pos+4] = u8(v >> 32); out[pos+5] = u8(v >> 40); out[pos+6] = u8(v >> 48); out[pos+7] = u8(v >> 56)
|
||||
pos += 8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -946,6 +976,12 @@ mem_matches_inline :: #force_inline proc "contextless" (op: ^Operand, op_type: O
|
||||
}
|
||||
|
||||
imm_matches_inline :: #force_inline proc "contextless" (op: ^Operand, op_type: Operand_Type) -> bool {
|
||||
// A labeled immediate is an address placeholder (movabs): it must select the
|
||||
// full imm64 form regardless of the label id's numeric value, so a small id
|
||||
// doesn't collapse to a shorter `mov r64, imm32` and lose the ABS64 slot.
|
||||
if op.flags.imm_is_label {
|
||||
return op_type == .IMM64
|
||||
}
|
||||
// Match based on whether the VALUE fits in the encoding's immediate size.
|
||||
// x64 immediates are interpreted as both signed and unsigned depending on context:
|
||||
// - ADD r32, imm8sx: sign-extended, so -1 becomes 0xFFFFFFFF
|
||||
|
||||
@@ -37,6 +37,11 @@ Memory :: bit_field u64 {
|
||||
addr_size_override: bool | 1,
|
||||
base_class: u8 | 5,
|
||||
index_class: u8 | 5,
|
||||
// When set, `disp` holds a label id (not a literal displacement): the
|
||||
// encoder emits a REL32 relocation at the disp32's byte offset instead of
|
||||
// the literal, so a RIP-relative operand can reference a label. Uses one of
|
||||
// Memory's 4 spare bits -- no growth, and one bit-test on the hot path.
|
||||
disp_is_label: bool | 1,
|
||||
}
|
||||
|
||||
@(require_results)
|
||||
@@ -76,6 +81,10 @@ mem_make :: proc "contextless" (base: Register, index: Register, scale: u8, disp
|
||||
}
|
||||
|
||||
mem.disp = disp
|
||||
// Must be explicitly cleared: `mem` starts uninitialized (`= ---`) and the
|
||||
// encoder reads this on the hot path -- garbage here would spuriously turn a
|
||||
// literal displacement into a label reference. mem_rip_label sets it after.
|
||||
mem.disp_is_label = false
|
||||
|
||||
return mem
|
||||
}
|
||||
@@ -150,6 +159,20 @@ mem_rip_disp :: #force_inline proc "contextless" (disp: i32) -> Memory {
|
||||
return mem_make(RIP, NONE, 1, disp, NONE)
|
||||
}
|
||||
|
||||
// RIP-relative memory operand whose displacement references a label rather than
|
||||
// a literal. The encoder writes a placeholder disp32 and emits a REL32
|
||||
// relocation for `label_id` at that field's byte offset -- the native way to
|
||||
// express `lea reg, [rip + <label>]` (position-independent data addressing).
|
||||
// The relocation carries addend 0; any within-section offset is supplied by the
|
||||
// downstream object/JIT layer when it resolves the label.
|
||||
@(require_results)
|
||||
mem_rip_label :: #force_inline proc "contextless" (label_id: u32) -> Memory {
|
||||
m := mem_make(RIP, NONE, 1, 0, NONE)
|
||||
m.disp = i32(label_id)
|
||||
m.disp_is_label = true
|
||||
return m
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// SECTION: 2.5 Operand struct and Flags
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -184,6 +207,10 @@ Operand_Flags :: bit_field u16 {
|
||||
zeroing: bool | 1, // merge (0) vs zero (1) masking
|
||||
broadcast: Broadcast | 3, // broadcast mode (see Broadcast enum)
|
||||
er_sae: u8 | 2, // embedded rounding / SAE (0=none, 1=RN-SAE, 2=RD-SAE, 3=RU-SAE/RZ-SAE)
|
||||
// When set on a movabs (IQ) immediate operand, `Operand.immediate` holds a
|
||||
// label id: the encoder writes a placeholder imm64 and emits an ABS64
|
||||
// relocation at its byte offset. Uses one of Operand_Flags' spare bits.
|
||||
imm_is_label: bool | 1,
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -255,6 +282,16 @@ op_label :: #force_inline proc "contextless" (label_id: u32, size: u8 = 4) -> Op
|
||||
return Operand{relative = i64(label_id), kind = .RELATIVE, size = size}
|
||||
}
|
||||
|
||||
// A movabs imm64 that references a label: the immediate stays kind .IMMEDIATE
|
||||
// (so form matching is unchanged) but is flagged as a label, so the encoder
|
||||
// emits an ABS64 relocation for it instead of a literal. Use for
|
||||
// `movabs reg, <label>` (absolute addressing; the target address is filled in
|
||||
// at relocation time).
|
||||
@(require_results)
|
||||
op_imm_label :: #force_inline proc "contextless" (label_id: u32) -> Operand {
|
||||
return Operand{immediate = i64(label_id), kind = .IMMEDIATE, size = 8, flags = {imm_is_label = true}}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// SECTION: 2.7 Typed Operand Constructors (compile-time type safety)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@@ -3369,6 +3369,153 @@ run_i386_tests :: proc() {
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SECTION 11: LABEL ADDRESSING (RIP-relative disp + movabs imm)
|
||||
// =============================================================================
|
||||
//
|
||||
// Native label references for a memory displacement (REL32) and a movabs imm64
|
||||
// (ABS64) -- the encoder emits the same relocations it already does for
|
||||
// jump/call targets, so a RIP-relative `lea reg, [rip + <label>]` and an
|
||||
// absolute `movabs reg, <label>` can be expressed directly.
|
||||
|
||||
read_i32_le :: proc(b: []u8, off: int) -> i32 {
|
||||
return i32(u32(b[off]) | (u32(b[off+1]) << 8) | (u32(b[off+2]) << 16) | (u32(b[off+3]) << 24))
|
||||
}
|
||||
|
||||
run_label_addressing_tests :: proc() {
|
||||
before := g_stats.failed
|
||||
|
||||
// -- lea rax, [rip + L], L DEFINED: disp32 must resolve to L - end_of_lea ----
|
||||
{
|
||||
// insts: 0=lea rax,[rip+L] 1=ret 2=ret L (id 0) at inst index 2.
|
||||
insts := []x86.Instruction{
|
||||
{mnemonic = .LEA, operand_count = 2, ops = {x86.op_reg(x86.RAX), x86.op_mem(x86.mem_rip_label(0), 8), {}, {}}},
|
||||
x86.inst_none(.RET),
|
||||
x86.inst_none(.RET),
|
||||
}
|
||||
labels := [1]x86.Label_Definition{x86.Label_Definition(2)} // L -> inst index 2
|
||||
code: [64]u8
|
||||
relocs: [dynamic]x86.Relocation; errs: [dynamic]x86.Error
|
||||
defer delete(relocs); defer delete(errs)
|
||||
|
||||
n, ok := x86.encode(insts, labels[:], code[:], &relocs, &errs, true, 0)
|
||||
// lea rax,[rip+disp32] = 48 8D 05 dd dd dd dd (7 bytes); disp32 at offset 3.
|
||||
disp := read_i32_le(code[:], 3)
|
||||
target := i32(labels[0]) // now a byte offset
|
||||
want := target - 7 // next_pc = end of the 7-byte lea
|
||||
prefix_ok := code[0] == 0x48 && code[1] == 0x8D && code[2] == 0x05
|
||||
if ok && len(relocs) == 0 && prefix_ok && disp == want && n == 9 {
|
||||
fmt.printf("%s[PASS]%s lea [rip+L] resolved: disp32=%d (L@%d)\n", GREEN, RESET, disp, target)
|
||||
g_stats.passed += 1
|
||||
} else {
|
||||
fmt.printf("%s[FAIL]%s lea [rip+L] resolved: ok=%v relocs=%d prefix_ok=%v disp=%d want=%d n=%d\n",
|
||||
RED, RESET, ok, len(relocs), prefix_ok, disp, want, n)
|
||||
g_stats.failed += 1
|
||||
}
|
||||
}
|
||||
|
||||
// -- lea rax, [rip + L], L UNDEFINED: a REL32 reloc at the disp32 offset ------
|
||||
{
|
||||
insts := []x86.Instruction{
|
||||
{mnemonic = .LEA, operand_count = 2, ops = {x86.op_reg(x86.RAX), x86.op_mem(x86.mem_rip_label(0), 8), {}, {}}},
|
||||
}
|
||||
labels := [1]x86.Label_Definition{x86.LABEL_UNDEFINED}
|
||||
code: [64]u8
|
||||
relocs: [dynamic]x86.Relocation; errs: [dynamic]x86.Error
|
||||
defer delete(relocs); defer delete(errs)
|
||||
|
||||
_, ok := x86.encode(insts, labels[:], code[:], &relocs, &errs, true, 0)
|
||||
r := len(relocs) == 1 ? relocs[0] : x86.Relocation{}
|
||||
good := ok && len(relocs) == 1 &&
|
||||
r.type == .REL32 && r.size == 4 && r.offset == 3 && r.label_id == 0 && r.addend == 0
|
||||
// placeholder disp must be zero until relocation
|
||||
good &&= read_i32_le(code[:], 3) == 0
|
||||
if good {
|
||||
fmt.printf("%s[PASS]%s lea [rip+L] unresolved: REL32 @off=%d size=%d label=%d\n",
|
||||
GREEN, RESET, r.offset, r.size, r.label_id)
|
||||
g_stats.passed += 1
|
||||
} else {
|
||||
fmt.printf("%s[FAIL]%s lea [rip+L] unresolved: ok=%v relocs=%d %v\n", RED, RESET, ok, len(relocs), r)
|
||||
g_stats.failed += 1
|
||||
}
|
||||
}
|
||||
|
||||
// -- movabs rax, L: an ABS64 reloc at the imm64 offset -----------------------
|
||||
{
|
||||
insts := []x86.Instruction{
|
||||
{mnemonic = .MOV, operand_count = 2, ops = {x86.op_reg(x86.RAX), x86.op_imm_label(0), {}, {}}},
|
||||
}
|
||||
labels := [1]x86.Label_Definition{x86.LABEL_UNDEFINED}
|
||||
code: [64]u8
|
||||
relocs: [dynamic]x86.Relocation; errs: [dynamic]x86.Error
|
||||
defer delete(relocs); defer delete(errs)
|
||||
|
||||
n, ok := x86.encode(insts, labels[:], code[:], &relocs, &errs, true, 0)
|
||||
r := len(relocs) == 1 ? relocs[0] : x86.Relocation{}
|
||||
// movabs rax, imm64 = 48 B8 <imm64> (10 bytes); imm64 at offset 2.
|
||||
prefix_ok := n == 10 && code[0] == 0x48 && code[1] == 0xB8
|
||||
good := ok && len(relocs) == 1 &&
|
||||
r.type == .ABS64 && r.size == 8 && r.offset == 2 && r.label_id == 0 && prefix_ok
|
||||
if good {
|
||||
fmt.printf("%s[PASS]%s movabs rax, L: ABS64 @off=%d size=%d (48 B8 ...)\n",
|
||||
GREEN, RESET, r.offset, r.size)
|
||||
g_stats.passed += 1
|
||||
} else {
|
||||
fmt.printf("%s[FAIL]%s movabs rax, L: ok=%v n=%d relocs=%d prefix_ok=%v %v\n",
|
||||
RED, RESET, ok, n, len(relocs), prefix_ok, r)
|
||||
g_stats.failed += 1
|
||||
}
|
||||
}
|
||||
|
||||
// -- lea [rip+L] with a nonzero-distance L, executed end-to-end --------------
|
||||
// Prove the resolved displacement actually points at the datum: lay a known
|
||||
// dword right after a `lea rax,[rip+L]; mov eax,[rax]; ret`, and check the
|
||||
// loaded value comes back. The disp is derived from the real layout, not baked.
|
||||
{
|
||||
DATUM :: i32(0x5A17C0DE)
|
||||
insts := []x86.Instruction{
|
||||
// 0: lea rax, [rip + L]
|
||||
{mnemonic = .LEA, operand_count = 2, ops = {x86.op_reg(x86.RAX), x86.op_mem(x86.mem_rip_label(0), 8), {}, {}}},
|
||||
// 1: mov eax, [rax] (load the datum L points at)
|
||||
{mnemonic = .MOV, operand_count = 2, ops = {x86.op_reg(x86.EAX), x86.op_mem(x86.mem_base_only(x86.RAX), 4), {}, {}}},
|
||||
// 2: ret
|
||||
x86.inst_none(.RET),
|
||||
// 3: filler that reserves >=4 bytes for the datum; its first 4 bytes are
|
||||
// overwritten below. Never executed (the ret at inst 2 returns first).
|
||||
x86.inst_r_i(.MOV, x86.EAX, 0x1111_1111, 4),
|
||||
}
|
||||
labels := [1]x86.Label_Definition{x86.Label_Definition(3)} // L at inst index 3
|
||||
code: [64]u8
|
||||
relocs: [dynamic]x86.Relocation; errs: [dynamic]x86.Error
|
||||
defer delete(relocs); defer delete(errs)
|
||||
|
||||
n, ok := x86.encode(insts, labels[:], code[:], &relocs, &errs, true, 0)
|
||||
// L's byte offset is where inst 3 starts; the lea's disp resolved to it.
|
||||
// Overwrite those 4 bytes with the datum the load should return.
|
||||
datum_off := int(labels[0])
|
||||
v := u32(DATUM)
|
||||
code[datum_off] = u8(v); code[datum_off+1] = u8(v >> 8); code[datum_off+2] = u8(v >> 16); code[datum_off+3] = u8(v >> 24)
|
||||
|
||||
exec := alloc_exec(4096)
|
||||
defer free_exec(exec)
|
||||
copy(exec, code[:n])
|
||||
fn := transmute(proc "c" () -> i64)raw_data(exec)
|
||||
got := i32(fn())
|
||||
if ok && len(relocs) == 0 && got == DATUM {
|
||||
fmt.printf("%s[PASS]%s lea [rip+L] loads datum: got 0x%08X\n", GREEN, RESET, u32(got))
|
||||
g_stats.passed += 1
|
||||
} else {
|
||||
fmt.printf("%s[FAIL]%s lea [rip+L] loads datum: ok=%v relocs=%d got 0x%08X want 0x%08X\n",
|
||||
RED, RESET, ok, len(relocs), u32(got), u32(DATUM))
|
||||
g_stats.failed += 1
|
||||
}
|
||||
}
|
||||
|
||||
if g_stats.failed == before {
|
||||
fmt.printf("%s[PASS]%s label addressing (REL32 disp + ABS64 movabs)\n", GREEN, RESET)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN
|
||||
// =============================================================================
|
||||
@@ -3413,6 +3560,9 @@ main :: proc() {
|
||||
|
||||
log_header("LARGE FUNCTION TESTS")
|
||||
run_large_tests()
|
||||
|
||||
log_header("LABEL ADDRESSING TESTS")
|
||||
run_label_addressing_tests()
|
||||
}
|
||||
|
||||
log_header("DECODE-ONLY TESTS")
|
||||
|
||||
Reference in New Issue
Block a user