diff --git a/linux-user/main.c b/linux-user/main.c
index c6f2e20..54970bc 100644
--- a/linux-user/main.c
+++ b/linux-user/main.c
@@ -806,6 +806,9 @@ void cpu_loop(CPUARMState *env)
                 }
             }
             break;
+        case EXCP_SEMIHOST:
+            env->regs[0] = do_arm_semihosting(env);
+            break;
         case EXCP_INTERRUPT:
             /* just indicate that signals should be handled asap */
             break;
diff --git a/target-arm/cpu.h b/target-arm/cpu.h
index 6695390..9d75227 100644
--- a/target-arm/cpu.h
+++ b/target-arm/cpu.h
@@ -52,7 +52,7 @@
 #define EXCP_SMC            13   /* Secure Monitor Call */
 #define EXCP_VIRQ           14
 #define EXCP_VFIQ           15
-#define EXCP_SEMIHOST       16   /* semihosting call (A64 only) */
+#define EXCP_SEMIHOST       16   /* semihosting call */
 
 #define ARMV7M_EXCP_RESET   1
 #define ARMV7M_EXCP_NMI     2
diff --git a/target-arm/helper.c b/target-arm/helper.c
index cb83ee2..25b15dc 100644
--- a/target-arm/helper.c
+++ b/target-arm/helper.c
@@ -6573,12 +6573,19 @@ static inline bool check_for_semihosting(CPUState *cs)
         /* Only intercept calls from privileged modes, to provide some
          * semblance of security.
          */
-        if (!semihosting_enabled() ||
-            ((env->uncached_cpsr & CPSR_M) == ARM_CPU_MODE_USR)) {
+        if (cs->exception_index != EXCP_SEMIHOST &&
+            (!semihosting_enabled() ||
+             ((env->uncached_cpsr & CPSR_M) == ARM_CPU_MODE_USR))) {
             return false;
         }
 
         switch (cs->exception_index) {
+        case EXCP_SEMIHOST:
+            /* This is always a semihosting call; the "is this usermode"
+             * and "is semihosting enabled" checks have been done at
+             * translate time.
+             */
+            break;
         case EXCP_SWI:
             /* Check for semihosting interrupt.  */
             if (env->thumb) {
diff --git a/target-arm/translate.c b/target-arm/translate.c
index 164b52a..ef62f8b 100644
--- a/target-arm/translate.c
+++ b/target-arm/translate.c
@@ -28,6 +28,7 @@
 #include "qemu/log.h"
 #include "qemu/bitops.h"
 #include "arm_ldst.h"
+#include "exec/semihost.h"
 
 #include "exec/helper-proto.h"
 #include "exec/helper-gen.h"
@@ -1144,6 +1145,33 @@ static inline void gen_lookup_tb(DisasContext *s)
     s->is_jmp = DISAS_JUMP;
 }
 
+static inline void gen_hlt(DisasContext *s, int imm)
+{
+    /* HLT. This has two purposes.
+     * Architecturally, it is an external halting debug instruction.
+     * Since QEMU doesn't implement external debug, we treat this as
+     * it is required for halting debug disabled: it will UNDEF.
+     * Secondly, "HLT 0x3C" is a T32 semihosting trap instruction,
+     * and "HLT 0xF000" is an A32 semihosting syscall. These traps
+     * must trigger semihosting even for ARMv7 and earlier, where
+     * HLT was an undefined encoding.
+     * In system mode, we don't allow userspace access to
+     * semihosting, to provide some semblance of security
+     * (and for consistency with our 32-bit semihosting).
+     */
+    if (semihosting_enabled() &&
+#ifndef CONFIG_USER_ONLY
+        s->current_el != 0 &&
+#endif
+        (imm == (s->thumb ? 0x3c : 0xf000))) {
+        gen_exception_internal_insn(s, 0, EXCP_SEMIHOST);
+        return;
+    }
+
+    gen_exception_insn(s, s->thumb ? 2 : 4, EXCP_UDEF, syn_uncategorized(),
+                       default_exception_el(s));
+}
+
 static inline void gen_add_data_offset(DisasContext *s, unsigned int insn,
                                        TCGv_i32 var)
 {
@@ -8395,6 +8423,10 @@ static void disas_arm_insn(DisasContext *s, unsigned int insn)
         {
             int imm16 = extract32(insn, 0, 4) | (extract32(insn, 8, 12) << 4);
             switch (op1) {
+            case 0:
+                /* HLT */
+                gen_hlt(s, imm16);
+                break;
             case 1:
                 /* bkpt */
                 ARCH(5);
@@ -8419,7 +8451,7 @@ static void disas_arm_insn(DisasContext *s, unsigned int insn)
                 gen_smc(s);
                 break;
             default:
-                goto illegal_op;
+                g_assert_not_reached();
             }
             break;
         }
@@ -11451,19 +11483,33 @@ static void disas_thumb_insn(CPUARMState *env, DisasContext *s)
             break;
         }
 
-        case 0xa: /* rev */
+        case 0xa: /* rev, and hlt */
+        {
+            int op1 = extract32(insn, 6, 2);
+
+            if (op1 == 2) {
+                /* HLT */
+                int imm6 = extract32(insn, 0, 6);
+
+                gen_hlt(s, imm6);
+                break;
+            }
+
+            /* Otherwise this is rev */
             ARCH(6);
             rn = (insn >> 3) & 0x7;
             rd = insn & 0x7;
             tmp = load_reg(s, rn);
-            switch ((insn >> 6) & 3) {
+            switch (op1) {
             case 0: tcg_gen_bswap32_i32(tmp, tmp); break;
             case 1: gen_rev16(tmp); break;
             case 3: gen_revsh(tmp); break;
-            default: goto illegal_op;
+            default:
+                g_assert_not_reached();
             }
             store_reg(s, rd, tmp);
             break;
+        }
 
         case 6:
             switch ((insn >> 5) & 7) {
