diff options
Diffstat (limited to 'arch/arm64/kernel/insn.c')
| -rw-r--r-- | arch/arm64/kernel/insn.c | 304 | 
1 files changed, 304 insertions, 0 deletions
diff --git a/arch/arm64/kernel/insn.c b/arch/arm64/kernel/insn.c new file mode 100644 index 00000000000..92f36835486 --- /dev/null +++ b/arch/arm64/kernel/insn.c @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2013 Huawei Ltd. + * Author: Jiang Liu <liuj97@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <http://www.gnu.org/licenses/>. + */ +#include <linux/bitops.h> +#include <linux/compiler.h> +#include <linux/kernel.h> +#include <linux/smp.h> +#include <linux/stop_machine.h> +#include <linux/uaccess.h> +#include <asm/cacheflush.h> +#include <asm/insn.h> + +static int aarch64_insn_encoding_class[] = { +	AARCH64_INSN_CLS_UNKNOWN, +	AARCH64_INSN_CLS_UNKNOWN, +	AARCH64_INSN_CLS_UNKNOWN, +	AARCH64_INSN_CLS_UNKNOWN, +	AARCH64_INSN_CLS_LDST, +	AARCH64_INSN_CLS_DP_REG, +	AARCH64_INSN_CLS_LDST, +	AARCH64_INSN_CLS_DP_FPSIMD, +	AARCH64_INSN_CLS_DP_IMM, +	AARCH64_INSN_CLS_DP_IMM, +	AARCH64_INSN_CLS_BR_SYS, +	AARCH64_INSN_CLS_BR_SYS, +	AARCH64_INSN_CLS_LDST, +	AARCH64_INSN_CLS_DP_REG, +	AARCH64_INSN_CLS_LDST, +	AARCH64_INSN_CLS_DP_FPSIMD, +}; + +enum aarch64_insn_encoding_class __kprobes aarch64_get_insn_class(u32 insn) +{ +	return aarch64_insn_encoding_class[(insn >> 25) & 0xf]; +} + +/* NOP is an alias of HINT */ +bool __kprobes aarch64_insn_is_nop(u32 insn) +{ +	if (!aarch64_insn_is_hint(insn)) +		return false; + +	switch (insn & 0xFE0) { +	case AARCH64_INSN_HINT_YIELD: +	case AARCH64_INSN_HINT_WFE: +	case AARCH64_INSN_HINT_WFI: +	case AARCH64_INSN_HINT_SEV: +	case AARCH64_INSN_HINT_SEVL: +		return false; +	default: +		return true; +	} +} + +/* + * In ARMv8-A, A64 instructions have a fixed length of 32 bits and are always + * little-endian. + */ +int __kprobes aarch64_insn_read(void *addr, u32 *insnp) +{ +	int ret; +	u32 val; + +	ret = probe_kernel_read(&val, addr, AARCH64_INSN_SIZE); +	if (!ret) +		*insnp = le32_to_cpu(val); + +	return ret; +} + +int __kprobes aarch64_insn_write(void *addr, u32 insn) +{ +	insn = cpu_to_le32(insn); +	return probe_kernel_write(addr, &insn, AARCH64_INSN_SIZE); +} + +static bool __kprobes __aarch64_insn_hotpatch_safe(u32 insn) +{ +	if (aarch64_get_insn_class(insn) != AARCH64_INSN_CLS_BR_SYS) +		return false; + +	return	aarch64_insn_is_b(insn) || +		aarch64_insn_is_bl(insn) || +		aarch64_insn_is_svc(insn) || +		aarch64_insn_is_hvc(insn) || +		aarch64_insn_is_smc(insn) || +		aarch64_insn_is_brk(insn) || +		aarch64_insn_is_nop(insn); +} + +/* + * ARM Architecture Reference Manual for ARMv8 Profile-A, Issue A.a + * Section B2.6.5 "Concurrent modification and execution of instructions": + * Concurrent modification and execution of instructions can lead to the + * resulting instruction performing any behavior that can be achieved by + * executing any sequence of instructions that can be executed from the + * same Exception level, except where the instruction before modification + * and the instruction after modification is a B, BL, NOP, BKPT, SVC, HVC, + * or SMC instruction. + */ +bool __kprobes aarch64_insn_hotpatch_safe(u32 old_insn, u32 new_insn) +{ +	return __aarch64_insn_hotpatch_safe(old_insn) && +	       __aarch64_insn_hotpatch_safe(new_insn); +} + +int __kprobes aarch64_insn_patch_text_nosync(void *addr, u32 insn) +{ +	u32 *tp = addr; +	int ret; + +	/* A64 instructions must be word aligned */ +	if ((uintptr_t)tp & 0x3) +		return -EINVAL; + +	ret = aarch64_insn_write(tp, insn); +	if (ret == 0) +		flush_icache_range((uintptr_t)tp, +				   (uintptr_t)tp + AARCH64_INSN_SIZE); + +	return ret; +} + +struct aarch64_insn_patch { +	void		**text_addrs; +	u32		*new_insns; +	int		insn_cnt; +	atomic_t	cpu_count; +}; + +static int __kprobes aarch64_insn_patch_text_cb(void *arg) +{ +	int i, ret = 0; +	struct aarch64_insn_patch *pp = arg; + +	/* The first CPU becomes master */ +	if (atomic_inc_return(&pp->cpu_count) == 1) { +		for (i = 0; ret == 0 && i < pp->insn_cnt; i++) +			ret = aarch64_insn_patch_text_nosync(pp->text_addrs[i], +							     pp->new_insns[i]); +		/* +		 * aarch64_insn_patch_text_nosync() calls flush_icache_range(), +		 * which ends with "dsb; isb" pair guaranteeing global +		 * visibility. +		 */ +		atomic_set(&pp->cpu_count, -1); +	} else { +		while (atomic_read(&pp->cpu_count) != -1) +			cpu_relax(); +		isb(); +	} + +	return ret; +} + +int __kprobes aarch64_insn_patch_text_sync(void *addrs[], u32 insns[], int cnt) +{ +	struct aarch64_insn_patch patch = { +		.text_addrs = addrs, +		.new_insns = insns, +		.insn_cnt = cnt, +		.cpu_count = ATOMIC_INIT(0), +	}; + +	if (cnt <= 0) +		return -EINVAL; + +	return stop_machine(aarch64_insn_patch_text_cb, &patch, +			    cpu_online_mask); +} + +int __kprobes aarch64_insn_patch_text(void *addrs[], u32 insns[], int cnt) +{ +	int ret; +	u32 insn; + +	/* Unsafe to patch multiple instructions without synchronizaiton */ +	if (cnt == 1) { +		ret = aarch64_insn_read(addrs[0], &insn); +		if (ret) +			return ret; + +		if (aarch64_insn_hotpatch_safe(insn, insns[0])) { +			/* +			 * ARMv8 architecture doesn't guarantee all CPUs see +			 * the new instruction after returning from function +			 * aarch64_insn_patch_text_nosync(). So send IPIs to +			 * all other CPUs to achieve instruction +			 * synchronization. +			 */ +			ret = aarch64_insn_patch_text_nosync(addrs[0], insns[0]); +			kick_all_cpus_sync(); +			return ret; +		} +	} + +	return aarch64_insn_patch_text_sync(addrs, insns, cnt); +} + +u32 __kprobes aarch64_insn_encode_immediate(enum aarch64_insn_imm_type type, +				  u32 insn, u64 imm) +{ +	u32 immlo, immhi, lomask, himask, mask; +	int shift; + +	switch (type) { +	case AARCH64_INSN_IMM_ADR: +		lomask = 0x3; +		himask = 0x7ffff; +		immlo = imm & lomask; +		imm >>= 2; +		immhi = imm & himask; +		imm = (immlo << 24) | (immhi); +		mask = (lomask << 24) | (himask); +		shift = 5; +		break; +	case AARCH64_INSN_IMM_26: +		mask = BIT(26) - 1; +		shift = 0; +		break; +	case AARCH64_INSN_IMM_19: +		mask = BIT(19) - 1; +		shift = 5; +		break; +	case AARCH64_INSN_IMM_16: +		mask = BIT(16) - 1; +		shift = 5; +		break; +	case AARCH64_INSN_IMM_14: +		mask = BIT(14) - 1; +		shift = 5; +		break; +	case AARCH64_INSN_IMM_12: +		mask = BIT(12) - 1; +		shift = 10; +		break; +	case AARCH64_INSN_IMM_9: +		mask = BIT(9) - 1; +		shift = 12; +		break; +	default: +		pr_err("aarch64_insn_encode_immediate: unknown immediate encoding %d\n", +			type); +		return 0; +	} + +	/* Update the immediate field. */ +	insn &= ~(mask << shift); +	insn |= (imm & mask) << shift; + +	return insn; +} + +u32 __kprobes aarch64_insn_gen_branch_imm(unsigned long pc, unsigned long addr, +					  enum aarch64_insn_branch_type type) +{ +	u32 insn; +	long offset; + +	/* +	 * PC: A 64-bit Program Counter holding the address of the current +	 * instruction. A64 instructions must be word-aligned. +	 */ +	BUG_ON((pc & 0x3) || (addr & 0x3)); + +	/* +	 * B/BL support [-128M, 128M) offset +	 * ARM64 virtual address arrangement guarantees all kernel and module +	 * texts are within +/-128M. +	 */ +	offset = ((long)addr - (long)pc); +	BUG_ON(offset < -SZ_128M || offset >= SZ_128M); + +	if (type == AARCH64_INSN_BRANCH_LINK) +		insn = aarch64_insn_get_bl_value(); +	else +		insn = aarch64_insn_get_b_value(); + +	return aarch64_insn_encode_immediate(AARCH64_INSN_IMM_26, insn, +					     offset >> 2); +} + +u32 __kprobes aarch64_insn_gen_hint(enum aarch64_insn_hint_op op) +{ +	return aarch64_insn_get_hint_value() | op; +} + +u32 __kprobes aarch64_insn_gen_nop(void) +{ +	return aarch64_insn_gen_hint(AARCH64_INSN_HINT_NOP); +}  | 
