0%

前言

一、健力与健美

核心目标 (The Core Goal)

  • 健力 (Powerlifting): 最大化绝对力量。目标非常明确,就是在三个标准动作上(深蹲、卧推、硬拉)举起一次所能举起的最大重量(1 Rep Max, 1RM)。比赛就是看谁的总重量最高。
  • 健美 (Bodybuilding): 最大化肌肉美感。目标是发展肌肉的尺寸(围度)、对称性、比例和清晰度(低体脂)。比赛是主观的,裁判根据选手的肌肉形态打分。

训练细节对比

方面 (Aspect) 健力 (Powerlifting) 健美 (Bodybuilding)
训练目标 (Training Goal) 神经系统适应肌力增长 让身体学会如何最高效地募集肌纤维来发力。 肌肉肥大 (Hypertrophy) 通过破坏和修复肌肉纤维,使其在体积上增长。
核心动作 (Core Lifts) 深蹲、卧推、硬拉 是训练的绝对核心,一切都围绕它们展开。 动作多样化,包括复合动作和大量孤立动作(如弯举、腿屈伸、侧平举)来雕刻特定的小肌群。
训练强度 (重量) (Intensity / Weight) 高强度。通常使用 >85% 1RM 的重量。训练的核心是举起非常重的重量。 中等强度。通常使用 60%-80% 1RM 的重量。重点不是举多重,而是给肌肉足够的刺激。
训练容量 (次数和组数) (Volume: Reps & Sets) 低次数,多组数。典型的模式是 1-5 次/组,进行 3-6 组。总次数较低,保证每次动作的质量。 高次数,多组数。典型的模式是 8-15 次/组,进行 3-5 组。追求更高的总容量来刺激肌肉生长。
组间休息 (Rest Between Sets) 长休息。通常为 3-5 分钟甚至更长。需要让中枢神经系统(CNS)和能量系统(ATP-PC)充分恢复,以保证下一组能继续使用大重量。 短休息。通常为 30-90 秒。目的是保持肌肉的持续紧张和代谢压力,追求“泵感”(Pump),这对肌肉肥大很重要。
训练频率 (Training Frequency) 高频率。一个动作(如深蹲)可能每周训练 2-3 次,通过不同强度的变化来不断优化技术和刺激力量增长。 低频率。通常采用“分化训练”(Split Training),例如“胸-三头”、“背-二头”、“腿”。每个肌群每周只深度刺激一次,然后给予充足时间(48-72小时)恢复和生长。
动作选择 (Exercise Selection) 围绕三大项,辅助动作的目的是加强薄弱环节。例如,用暂停卧推来加强胸部启动力量,用罗马尼亚硬拉来加强腘绳肌。 动作选择非常广泛,目的是从不同角度刺激同一块肌肉。例如,练胸可能会做上斜、平板、下斜的卧推、飞鸟等,确保胸肌的每个部分都得到发展。
关注点 (Mindset) (Focus) 动作效率和技术。思考如何用最省力、最稳定的方式移动重量,力求完美的技术模式。 **念动一致 (Mind-Muscle Connection)**。专注于感受目标肌肉的收缩和拉伸,确保是目标肌肉在发力,而不是单纯地移动重量。追求肌肉的“灼烧感”和“泵感”。

举个例子:练胸日

  • 一个健力运动员的练胸日可能看起来像这样:
    1. 主项 - 卧推: 5 组 x 3 次 @ 90% 1RM (组间休 4 分钟)
    2. 辅助项 - 窄距卧推: 4 组 x 5 次 (加强三头肌) (组间休 2-3 分钟)
    3. 辅助项 - 杠铃划船: 4 组 x 6 次 (加强上背部以稳定卧推) (组间休 2-3 分钟)
  • 一个健美运动员的练胸日可能看起来像这样:
    1. 上斜杠铃卧推: 4 组 x 8-10 次 (组间休 90 秒)
    2. 平板哑铃卧推: 4 组 x 10-12 次 (组间休 60-90 秒)
    3. 器械夹胸 (飞鸟机): 3 组 x 12-15 次 (组间休 60 秒)
    4. 双杠臂屈伸 (偏重胸): 3 组 x 力竭 (组间休 60 秒)
    5. 绳索夹胸: 3 组 x 15 次 (最后做个超级组) (组间休 45 秒)

总结与建议

  • 健力问的是:“我能举起多重?”

  • 健美问的是:“我的肌肉看起来怎么样?”


二、人体供能体系

最终都是ATP 供能

特征 磷酸原系统 (ATP-CP) 糖酵解系统 (Glycolytic) 有氧氧化系统 (Aerobic Oxidative)
主要燃料 ATP(现存)和肌酸磷酸 (CP) 葡萄糖 (Glycogen, 糖原) 碳水化合物 (糖原)、脂肪、少量蛋白质
供能速度 最快 较快 最慢
供能容量/持续时间 最低 (约 5-10 秒) 中等 (约 30 秒 - 2 分钟) 最高 (数小时甚至更长)
是否需要氧 无氧 (Anaerobic) 无氧 (Anaerobic) 有氧 (Aerobic)
反应 ATP(三磷酸腺苷)转换为ADP(二磷酸腺苷)+ Pi(无机磷酸),并释放能量
ATP
代谢产物 热量、ADP(易于转化回ATP) 乳酸 (Lactate) 和氢离子 (H+),导致肌肉酸痛疲劳 二氧化碳 (CO2) 和水 (H2O)
典型活动 极限冲刺 (100米跑)、举重 1RM、跳跃、爆发性动作 400米跑、剧烈间歇训练、高强度力量训练(多组) 马拉松、长距离骑行、游泳、日常低强度活动
  • 有氧系统是基础: 有氧氧化系统是所有能量系统的基础。它的效率决定了身体清除乳酸、恢复磷酸原储备以及在低强度下持续运动的能力。有氧能力越强,身体在无氧运动后的恢复速度越快。
  • 无氧系统突破极限: 无氧系统(磷酸原和糖酵解)则允许我们在短时间内超越有氧系统的供能极限,进行更高强度的运动

总结与建议:

  • 想变强壮 (健力)? 训练你的 磷酸原系统。采用大重量、低次数、长间歇的训练。
  • 想增肌塑形 (健美)? 主要训练你的 糖酵解系统。采用中等重量、中高次数、短间歇的训练,追求“泵感”和代谢压力。
  • 想跑得更久 (耐力)? 训练你的 有氧氧化系统。进行长时间、中低强度的有氧运动。
  • 想全面发展 (如 CrossFit 运动员)? 你的训练计划必须 全面覆盖 这三个系统,既要有力量日,也要有高强度间歇日和长距离有氧日。

三、规范动作

我们来把肌肉训练想象成 “盖一栋高质量的房子”

  • 基础部分(沉肩、收腹、动作标准) 就是 “打地基和搭建框架”。如果地基不稳,框架不正,你用的砖(能量)再好,房子也会是危楼,甚至会倒塌(受伤)。
  • 能量部分(发力、行程、做功) 就是 “搬运和砌砖” 的过程。你要用多大的力气(发力)、把砖搬多远(行程),最终决定了你砌了多少墙(做功),也就是对肌肉产生了多少有效刺激。

下面我们来详细拆解每一项的意义和影响。

这部分决定了你的训练是否 安全、稳定、精准

1. 沉肩 (Scapular Depression & Retraction)

  • 是什么: 简单说就是让你的肩胛骨 向后收紧并向下压,远离耳朵。想象一下你要用两个肩胛骨去夹住一支笔。
  • 意义和影响:
    • 建立稳定的平台: 肩关节是人体最灵活但也最不稳定的关节。沉肩能将肩胛骨“锁”在胸廓上,为手臂发力提供一个极其稳固的支点。就像大炮需要一个坚实的炮架才能精准发射,否则力量会四处泄散。
    • 精准刺激目标肌群:
      • 卧推时: 沉肩能更好地孤立胸大肌发力,防止三角肌前束(肩膀)代偿过多,既能提升推起重量的效率,又能保护肩关节。
      • 划船/引体向上时: 沉肩是启动背阔肌的第一步。没有沉肩,你只是在用手臂和斜方肌上束“拉”,而不是用“背”在拉。
    • 预防伤害: 这是最重要的一点。不沉肩进行推、拉动作,会让肱骨头和肩峰产生撞击和摩擦,极易导致 肩峰下撞击综合征,也就是健身中最常见的肩痛。

2. 收腹 (Core Engagement)

  • 是什么: 不是简单地把肚子吸进去。而是主动收紧腹部、腰部和臀部的所有肌肉,让整个核心区像一个坚固的“木桶”,而不是一根柔软的“面条”。想象有人要打你一拳,你瞬间绷紧肚子的感觉。
  • 意义和影响:
    • 保护脊柱: 核心收紧能极大增加腹内压,像一个“天然腰带”一样稳定和保护你的腰椎。这是做大重量深蹲和硬拉时防止腰部受伤的生命线。
    • 高效的力量传导: 力量从地面通过腿部,需要经过核心区才能传导到上肢和杠铃上。如果核心是松的,力量就会在这里“漏掉”,导致你举起的重量大打折扣。一个稳定的核心能让全身的力量拧成一股绳。
    • 提升全身的稳定性: 即使是做弯举、推举这类看似和核心无关的动作,收紧腹部也能提供更好的全身稳定性,让你能更专注于目标肌肉的发力。

3. 动作标准 (Proper Form)

  • 是什么: 这是以上所有基础的总和。它意味着在 正确的发力模式、完整的运动行程和可控的速度 下,完成每一次动作。
  • 意义和影响:
    • 有效性的保证: 只有动作标准,你施加的负荷才能准确地作用于你想练的肌肉上。错误的动作只会让你“练偏”,甚至练到根本不想练的部位。
    • 安全性的前提: 动作标准是避免受伤的第一道,也是最重要的一道防线。几乎所有的训练伤害都源于动作模式的崩溃。
    • 可持续发展的基石: 良好的动作模式可以让你在未来几年甚至几十年里持续进步。而坏的习惯一旦养成,不仅会限制你的发展,修正起来也极其困难。

四、能量、刺激

这部分决定了你对肌肉施加了 多少有效的刺激

这是肌肉增长的直接原因 (The Energy Aspect: The Direct Cause of Growth)

1. 发力 (Force Generation)

  • 是什么: 不仅仅是把重量举起来,而是 “如何”把重量举起来。它关乎你的 “念动一致”(Mind-Muscle Connection)
  • 对训练效果的影响:
    • 有效刺激 vs 无效移动: 同样是做二头弯举,你是感觉到二头肌像拧毛巾一样收缩,把哑铃“卷”起来?还是用身体的晃动和肩膀的力量把哑铃“甩”起来?前者是高效发力,每一分力都作用在目标肌肉上;后者只是在移动重物,对二头肌的刺激微乎其微。
    • 向心与离心收缩: 一个完整的动作包含发力举起(向心)和缓慢下放(离心)。高质量的发力不仅关注“举”,更关注“放”。可控的离心收缩(下放过程)能造成更多的肌肉微小撕裂,是刺激肌肉生长的关键。

2. 行程 (Range of Motion - ROM)

  • 是什么: 指的是在一次动作中,负重移动的完整距离。例如,深蹲是从站直下到大腿平行于地面(或更低),再回到站直。
  • 对训练效果的影响:
    • 更充分的肌肉拉伸和收缩: 完整的行程能让目标肌肉经历从最长(拉伸)到最短(收缩)的整个过程。这能募集到更多的肌纤维参与工作,刺激更全面。例如,全范围的深蹲比半蹲更能刺激到臀大肌和股四头肌的下沿。
    • 提升关节灵活性和健康: 在可控的情况下做全范围动作,本身就是一种动态拉伸,有助于维持和改善关节的活动度。
    • 注意: 有时也会策略性地使用 “部分行程”,比如为了冲击大重量或者针对肌肉的某个特定部分进行强化,但对于大多数增肌训练,全范围行程是黄金标准

3. 做功 (Work)

  • 是什么: 在物理学上,**做功 = 力 × 距离 (W = F × d)**。在健身中,我们可以简单理解为 做功 ≈ 重量 × 次数 × 组数 × 行程。这代表了你一次训练中完成的 **总训练量 (Total Volume)**。
  • 对训练效果的影响:
    • 肌肉增长的核心驱动力: 肌肉为什么要生长?因为它要适应你给它的“工作压力”。你做的功越多,对肌肉施加的压力就越大,身体接收到的“需要变强壮”的信号就越强烈。这就是 “渐进超负荷” 原则的量化体现。
    • 如何增加做功:
      1. 增加“力”(Force): 举起更重的重量。
      2. 增加“距离”(Distance): 保证完整的行程。
      3. 增加重复次数或组数: 这直接增加了总的做功量。
    • 高质量的做功: 仅仅追求数字上的“总做功量”是不够的。在动作标准(基础)的前提下,最大化有效做功,才是高效训练的关键。用错误的动作做再多的功,也只是在增加受伤风险,而不是肌肉。

总结:如何将它们结合

  1. 第一步,建立地基: 在任何重量下,首先确保 沉肩、收腹,保证身体框架的稳定。
  2. 第二步,校准框架: 学习并掌握每个动作的 标准模式,知道哪里该发力,哪里该稳定。
  3. 第三步,开始砌墙:
    • 用强烈的 “念动一致”发力,感受目标肌肉的收缩。
    • 在可控的前提下,走完 完整的行程,让肌肉得到充分刺激。
    • 通过逐渐增加重量、次数或组数,来提升你的 总做功量,从而实现渐进超负荷。

记住这个逻辑链:
稳固的基础 → 标准的动作 → 高质量的发力 → 完整的行程 → 有效的总做功 → 肌肉的生长与力量的提升

当你把这几个方面都融入到每一次的训练中,你的训练质量和效果将会发生质的飞跃。

A、核心概念

渐进超负荷 (Progressive Overload)

这是一个关于 “身体为何要改变” 的根本原则.

渐进超负荷 是指为了持续获得肌肉增长、力量提升或耐力增强,你必须不断地、逐渐地给你身体施加比它已适应的更大、更强的刺激。

1. 定义:

渐进超负荷是肌肉生长和力量进步的最核心、最基本原则。它指的是为了持续刺激肌肉适应性增长,你需要循序渐进地增加训练对肌肉施加的负荷或压力。简单来说,就是让你的肌肉定期面对比之前更大的挑战。

2. 为什么需要渐进超负荷?

肌肉和身体是高度适应性的。当你对肌肉施加一个刺激(如举起一定重量),它们会通过生长和变强来适应这个刺激。一旦它们适应了,同样的刺激就不会再引起进一步的适应性变化。为了继续进步,你需要提供一个新的、更大的刺激,打破肌肉的“舒适区”,迫使其再次适应,从而实现持续的增长。

一个绝佳的比喻: 你的手掌为什么会长茧?因为你第一次干重活时,皮肤受到了它不习惯的压力(超负荷),于是它破了、疼了。但为了适应下一次同样的压力,它会“超量恢复”,长出更厚、更坚韧的皮肤(茧)。如果你从此只干这点活,茧就不会再变厚。但如果你开始干更重的活(渐进超负荷),你的手就会长出更厚的茧来适应。

肌肉的生长和力量的提升遵循完全相同的逻辑。身体很“懒”(或者说很高效),它只会为了适应你给它的挑战而改变。如果你永远用10公斤的哑铃做弯举,你的手臂在适应了这个重量后,就不会有任何理由继续变粗变壮。

3. 如何实践渐进超负荷?(重点)

增加负荷 不仅仅是增加重量,有很多种方法:

  1. 增加重量 (Intensity): 这是最经典的方法。这次你用20公斤卧推8次,下次尝试用22.5公斤推6-8次。
  2. 增加次数 (Reps): 重量不变,增加每组的重复次数。这次你用20公斤卧推8次,下次尝试推9次或10次。
  3. 增加组数 (Volume): 重量和次数不变,增加总组数。这次你卧推3组,下次尝试做4组。
  4. 缩短组间休息 (Density): 用同样的时间完成更多的训练量,增加代谢压力。原来休息90秒,下次尝试休息75秒。
  5. 增加训练频率 (Frequency): 原来每周练一次胸,现在尝试每周练两次(例如一次大重量,一次中等重量)。
  6. 提升动作质量 (Quality):
    • 减慢离心速度: 用更慢、更可控的速度下放重量。
    • 在顶峰收缩暂停: 在动作的最高点刻意挤压肌肉1-2秒。
    • 这两种方式都会在同样重量下,给肌肉带来巨大的新挑战。

关键: “渐进” (Progressive) 是核心。不要急于求成,每周、每次训练只追求微小的进步,但长期积累下来,效果是惊人的。

念动一致 (Mind-Muscle Connection, MMC)

这是一个关于 “训练质量和效率” 的核心技巧。

1. 定义:

念动一致 是指在进行训练时,将你的意识和注意力完全集中在正在发力的目标肌肉上,去主动地、刻意地感受它的拉伸与收缩。它是一种神经系统和肌肉系统之间建立起来的强大连接。

一个简单的比喻: 想象你的大脑是一个 聚光灯。在训练时,你可以选择把这盏灯打在“移动重量”这个任务上,也可以选择把灯打在“感受胸肌被挤压”这件事上。“念动一致”就是把聚光灯精准地打在目标肌肉上。

没有念动一致的训练,你只是在 **“移动重物” (Moving the weight)**。
拥有念动一致的训练,你才是在 **“训练肌肉” (Training the muscle)**。

2. 它为什么如此重要?

  • 最大化肌肉激活: 科学研究(通过肌电图EMG)表明,当受试者被要求专注于特定肌肉发力时,该肌肉的激活水平会显著高于不专注时。更高的激活水平意味着更多的肌纤维参与工作,从而带来更好的增肌效果。
  • 提升训练的有效性: 它可以确保是“对”的肌肉在工作,而不是让其他肌肉代偿。例如,做背部划船时,强烈的念动一致能让你用背阔肌去拉,而不是用二头肌和后束猛地一“拽”。
  • 降低受伤风险: 当你专注于目标肌肉时,你会更倾向于使用标准、可控的动作,而不是用惯性或错误的姿势去“甩”重量,这自然会更安全。
  • 让轻重量也变得有效: 当你的念动一致能力很强时,即使使用中等或较轻的重量,你也能给肌肉带来非常强烈的刺激,这对于关节不适或恢复期的人来说尤其重要。

3. 如何实践念动一致?(重点)

  1. 降低重量: 这是第一步也是最重要的一步。放下你的自尊心(Ego),用一个你能轻松控制的重量开始。
  2. 放慢速度: 特别是下放(离心)阶段,用3-4秒的时间慢慢下放,仔细感受肌肉被拉长的感觉。
  3. 顶峰收缩: 在动作的最高点(肌肉收缩最紧时)暂停1-2秒,主动地、用尽全力去“挤压”你的目标肌肉。
  4. 触觉反馈: 在安全的情况下,用另一只手去触摸你正在训练的肌肉。例如,做二头弯举时,用右手摸着左手的二头肌,感受它的隆起和紧绷。
  5. 知识和想象: 了解一点基础的解剖学,知道目标肌肉的起点、止点和功能。在训练时,想象那些肌纤维像绳索一样在收缩和拉长。
  6. 做单侧训练和孤立动作: 单臂弯举、单臂划船这类动作,能让你更容易集中注意力。

todo

肌肉群

纯粹文件访问:sshfs(wsl) + vscode 远程方案

传统使用vscode 提供的远程时, 服务器端的 vscode相关服务(node)作妖,磁盘io 拉满,造成系统卡顿,并影响其他服务

这个方案就是为了避免这种情况,当然,仅限于对于远程只有文件访问需求的场景

使用sshfs 挂载远程目录到wsl,然后使用vscode 远程

sshfs 挂载的目录访问权限仅限root,所以挂载点我放在了根目录

0. 相关工具

sshfs

1. 挂载远程目录

1
2
3
4
5
sudo su
mkdir -p /sshfs/www.cheerlisten.com
cd /sshfs

sshfs lull@www.cheerlisten.com:/ /sshfs/www.cheerlisten.com

2.

配置本地远程

A. wsl 配置默认用户

sudo vim /etc/wsl.conf

1
2
[user]
default=root

重启等待(8秒)生效

B. 本地配置ssh root

略过

3. sshfs 命令补充

上述挂载命令是临时的,重启后无效

如果要持久化,则需编辑 sudo vim /etc/fstab,添加如下行

1
lull@www.cheerlisten.com:/ /sshfs/www.cheerlisten.com fuse.sshfs defaults,_netdev,users,idmap=user,allow_other 0 0

另,付其他命令

1
2
3
4
5
6
7
8
9
10
11
12
# 查看 sshfs挂载情况
mount | grep fuse.sshfs
findmnt -t fuse.sshfs
df -hT | grep fuse.sshfs

# 添加挂载
mkdir -p /www.cheerlisten.com
sshfs lull@www.cheerlisten.com:/ /sshfs/www.cheerlisten.com

# 删除挂载点
fusermount3 -u /www.cheerlisten.com
umount /www.cheerlisten.com

纯粹文件访问:sshfs(wsl) + vscode 远程方案

传统使用vscode 提供的远程时, 服务器端的 vscode相关服务(node)作妖,磁盘io 拉满,造成系统卡顿,并影响其他服务

这个方案就是为了避免这种情况,当然,仅限于对于远程只有文件访问需求的场景

使用sshfs 挂载远程目录到wsl,然后使用vscode 远程

sshfs 挂载的目录访问权限仅限root,所以挂载点我放在了根目录

0. 相关工具

sshfs

1. 挂载远程目录

1
2
3
4
5
sudo su
mkdir -p /sshfs/www.cheerlisten.com
cd /sshfs

sshfs lull@www.cheerlisten.com:/ /sshfs/www.cheerlisten.com

2.

配置本地远程

A. wsl 配置默认用户

sudo vim /etc/wsl.conf

1
2
[user]
default=root

重启等待(8秒)生效

B. 本地配置ssh root

略过

3. sshfs 命令补充

上述挂载命令是临时的,重启后无效

如果要持久化,则需编辑 sudo vim /etc/fstab,添加如下行

1
lull@www.cheerlisten.com:/ /sshfs/www.cheerlisten.com fuse.sshfs defaults,_netdev,users,idmap=user,allow_other 0 0

另,付其他命令

1
2
3
4
5
6
7
8
9
10
11
12
# 查看 sshfs挂载情况
mount | grep fuse.sshfs
findmnt -t fuse.sshfs
df -hT | grep fuse.sshfs

# 添加挂载
mkdir -p /www.cheerlisten.com
sshfs lull@www.cheerlisten.com:/ /sshfs/www.cheerlisten.com

# 删除挂载点
fusermount3 -u /www.cheerlisten.com
umount /www.cheerlisten.com

二分写法

将一个区间 [l,r] 分为 [l,m][m + 1,r] 两个区间,左右区间元素通过bool Less(int idx) 区分

std::lower_boundstd::upper_bound的区别在哪里呢?

  • std::lower_bound: Less( {左区间元素}, val ) == true
  • std::upper_bound: Less( {右区间元素}, val ) == true

他们都返回右区间起始元素 ( end 是哨兵)

Version 1

二分顾名思义,在有序线性表上每次尝试去除一半的解空间

难以处理的边界值是当 当前范围为2的时候,下边是带注释的直觉的写法

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
T* lower_bound(vector<T>& arr, int val)
{
// [l,r] : possible result
int l = 0, r = arr.size() - 1;
while (l < r)
{
// About whether the mid should be closer to l or r
// this is mattered when l + 1 == r
int mid = l + (r - l) / 2;
// note since Less is operator<, we should always identify the less relation
// this would be LessT(arr[mid], val) for std::upper_bound
if (LessT(arr[mid], val))
l = mid + 1;
else
r = mid;
// take a look at the range shrinking process
// l = mid +1
// or
// r = mid
// since l = mid + 1 is always ok for range reduce
// so to keep r = mid always do so, the mid must be closer to l
}
if (!LessT(arr[l], val))
return arr.data() + l;
else
return arr.data() + arr.size();
}

Version 2

更加简洁优美的写法来自于 sentinel(哨兵)概念的引入, lower_bound 或者 upper_bound 为不存在的状态返回 arr.data() + arr.size(),这其实是一个天然哨兵

在上一个写法中,我们在末尾需要判断当前界值的有效性,这个判断摆在这里就有点多余。

一开始阻碍我将哨兵加入范围判断的思想来源于sentinel的不可访问性

但其实,不必真的访问哨兵

源码可以参考 msvc的实现

故有修改版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
T* lower_bound(vector<T>& arr, int val)
{
// [l,r] : possible retval
int l = 0, r = arr.size();
while (l < r)
{
int mid = l + (r - l) / 2;
if (LessT(arr[mid - 1], val))
l = mid + 1;
else
r = mid;
}
return arr.data() + l;
}

总结

二分的坑点有二:

  • mid 值的判断:这一步可以先不做处理,等到判断解空间分割的时候,通过观察子解空间必定缩小的条件来决定,mid应当靠近方向
  • 哨兵返回值:将尾元素值认为是绝对符合要求(大于或大于等于)的值

测试代码

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
#include <algorithm>
#include <cassert>
#include <iostream>
#include <vector>

using namespace std;
using T = int;
bool LessT(T a, T b)
{
return a < b;
}

struct BS
{
virtual T* lower_bound(vector<T>& arr, int val) = 0;
};
struct BS_std : public BS
{
T* lower_bound(vector<T>& arr, int val)
{
return std::lower_bound(arr.data(), arr.data() + arr.size(), val, LessT);
}
};

struct BS_V1 : public BS
{
T* lower_bound(vector<T>& arr, int val)
{
// [l,r] : possible result
int l = 0, r = arr.size() - 1;
while (l < r)
{
// About whether the mid should be closer to l or r
// this is mattered when l + 1 == r
int mid = l + (r - l) / 2;
// note since Less is operator<, we should always identify the less relation
// this would be LessT(arr[mid], val) for std::upper_bound
if (LessT(arr[mid], val))
l = mid + 1;
else
r = mid;
// take a look at the range shrinking process
// l = mid +1
// or
// r = mid
// since l = mid + 1 is always ok for range reduce
// so to keep r = mid always do so, the mid be closer to l would be nice.
}
if (!LessT(arr[l], val))
return arr.data() + l;
else
return arr.data() + arr.size();
}
};

struct BS_V2 : public BS
{
T* lower_bound(vector<T>& arr, int val)
{
// [l,r] : possible retval
int l = 0, r = arr.size();
while (l < r)
{
int mid = l + (r - l) / 2;
if (LessT(arr[mid - 1], val))
l = mid + 1;
else
r = mid;
}
return arr.data() + l;
}
};

int main()
{
int testRunCount = 1000;
int arrSize = 1000;
srand(time(0));

BS* bss[] = {
new BS_V1(),
new BS_V2(),
};
for (int r = 0; r < testRunCount; r++)
{
vector<T> data(arrSize);
for (int i = 0; i < arrSize; i++)
data[i] = rand() % (testRunCount * 2);
sort(data.begin(), data.end(), LessT);

for (int t = 0; t < 3; ++t)
{
int tval = rand() % (testRunCount * 2);
T* std_result = BS_std().lower_bound(data, tval);

for (auto bs : bss)
{
T* result = BS_V1().lower_bound(data, tval);
assert(std_result == result);
}
}
}
}

这一节分析协程状态结构,指的是 官方文档中的 coroutine state

the coroutine state, which is internal, dynamically-allocated storage (unless the allocation is optimized out), object that contains

  • the promise object
  • the parameters (all copied by value)
  • some representation of the current suspension point, so that a resume knows where to continue, and a destroy knows what local variables were in scope
  • local variables and temporaries whose lifetime spans the current suspension point.

姑且命名此结构为CoroState

先将分析结果贴出来,此结构布局只针对样例。
另一方面,因为coroutine与coroutine函数各自独立,所以可以猜测CoroState中与临时或本地变量有关的东西应该是指针或者状态式的。

CoroState 结构布局

type range naming meaning
void * 0-8 entry_ resume entry
void * 8-16 entry2_ not seen yet
void * 16-24 promise_ promise itself(no pointer)
void * 24-32 act_coro_ actived coro in its frame ?
int16_t 32-34 stat_ coro state see [coro state](#coro 状态)
bool 34-35 ifvalid_ flag indicates if coro frame is valid

coro 状态

这里有一个很重要的状态成员 stat_,先将状态含义贴出来,以便于阅读后边的代码

val mean
0 after call promise::get_return_object
2 after call promise::initial_suspend

汇编分析

结论(cc)

  1. CoroFunc 本身被gcc编译成空壳函数体,其内部的语句都编译在CoroFunc.Frame,即另一个代码区。其本身只负责初始化Coro帧并调用promise::initial_suspendpromise::get_return_objectcoroutine::resume. 伪码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    FORCE_NOINLINE coroutine CAwait1(){
    CoroState * frame = new CoroState;
    frame->ifValid_ =true;
    frame->entry_ = 0xaaa;// resume entry
    frame->entry2_ = 0xbbb;// another resume point
    auto pro =frame->promise_;
    pro->promise();// 初始化promise
    coroutine ret = pro->get_return_object(); // 拿到返回对象
    co_await pro->initial_suspend();
    }

compile config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cmake_minimum_required(VERSION 3.10)

set(${CMAKE_HOST_SYSTEM_NAME} TRUE)
set(CMAKE_CXX_STANDARD 20)


project(TEST)
aux_source_directory(. SRCS)

if(Linux)
add_compile_options(-fcoroutines)
message(CMAKE_CXX_FLAGS_DEBUG=${CMAKE_CXX_FLAGS_DEBUG})
set(CMAKE_CXX_FLAGS_DEBUG "-O1 -g -fno-exceptions")
# add_link_options("-T" "${CMAKE_CURRENT_LIST_DIR}/ls.ld")
endif()

include_directories(.)

foreach(src ${SRCS})
string(REGEX REPLACE ".*/([^/\\]*)\.cpp" "\\1" name "${src}")
message("src=${src} name=${name}")
add_executable(${name} ${src})
endforeach()

utils.hpp

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
#include <coroutine>
// #include <utils.hpp>

#pragma GCC push_options
#pragma GCC optimize("O0")
extern "C" FORCE_NOINLINE void TagFunc(int)
{
}
struct suspend_always_rt
{
constexpr bool await_ready() const noexcept
{
return false;
}

constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}

constexpr void await_resume() const noexcept {}
};
#pragma GCC pop_options

struct promise;
struct coroutine : std::coroutine_handle<promise>
{
using promise_type = ::promise;
};


extern "C" struct promise
{
FORCE_NOINLINE promise()
{
TagFunc(1);
}
FORCE_NOINLINE promise(int i_)
: i(i_)
{
TagFunc(2);
}
FORCE_NOINLINE coroutine get_return_object()
{
TagFunc(3);
return {coroutine::from_promise(*this)};
}
FORCE_NOINLINE suspend_always_rt initial_suspend() noexcept
// FORCE_NOINLINE std::suspend_always initial_suspend() noexcept
{
TagFunc(4);
return {};
}
FORCE_NOINLINE std::suspend_always final_suspend() noexcept
{
TagFunc(5);
return {};
}
FORCE_NOINLINE void return_void()
{
TagFunc(6);
}
FORCE_NOINLINE void unhandled_exception()
{
}
int i = 0;
};

FORCE_NOINLINE coroutine CAwait1()
{
TagFunc(7);
co_return;
}

int main()
{
auto c1 = CAwait1();
c1.resume();

}

Appendix

  • _Znwm and _ZdlPv 是 new 和delete的符号名

  • bt 指令: bit get

  • test 指令: 按位与操作,会设置 CF(carry flag)

这一篇介绍工程及环境配置

1
2
gcc version 11.4.0 (Ubuntu 11.4.0-1ubuntu1~22.04)
Description: Ubuntu 24.04 LTS

cmake 工程

  • why -fno-exceptions: 异常安全会在协程里插入一些处理代码,影响理解
  • why -O1: O0会产生很多冗余的指令,没有O1看着干净。当然我们也需要某些地方不加优化,参见 gcc 禁止优化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cmake_minimum_required(VERSION 3.10)

set(${CMAKE_HOST_SYSTEM_NAME} TRUE)
set(CMAKE_CXX_STANDARD 20)

project(TEST)
aux_source_directory(. SRCS)

if(Linux)
add_compile_options(-fcoroutines)
message(CMAKE_CXX_FLAGS_DEBUG=${CMAKE_CXX_FLAGS_DEBUG})
set(CMAKE_CXX_FLAGS_DEBUG "-O1 -g -fno-exceptions")
# add_link_options("-T" "${CMAKE_CURRENT_LIST_DIR}/ls.ld")
endif()

include_directories(.)

foreach(src ${SRCS})
string(REGEX REPLACE ".*/([^/\\]*)\.cpp" "\\1" name "${src}")
message("src=${src} name=${name}")
add_executable(${name} ${src})
endforeach()

调试

随意,但qtcreator的视图蛮不错(设置operator on instruction的快捷键),当然vscode也可以
vscode asm debug
qtcreator asm debug

utils

gcc 禁止优化

1
2
3
4
#pragma GCC                    push_options
#pragma GCC optimize("O0")
extern "C" FORCE_NOINLINE void TagFunc(int){}
#pragma GCC pop_options

我们将使用这个函数,作为代码间的tag,建立源码与汇编码之间的节点映射,如下使用

1
TagFunc(2);

这将被编译成
1
2
mov 0x02, %rdi
call TagFunc

utils.hpp code

1
2
3
4
5
6
7
// utils.hpp
#pragma once
#include <stdio.h>
#define FORCE_NOINLINE __attribute__((noinline))
#define LOG(...) printf(__VA_ARGS__)
#define WLOG(...) wfprintf(__VA_ARGS__)

Breathing & Traveling in the hard zone
Things are more interesting

这个系列将在汇编层分析gcc coroutine的实现,以巩固汇编与coro

前置知识

如果不熟悉基本的汇编,可以看这篇 X64汇编入门

如果不熟悉coroutine,可以看官方文档 coroutines

基本章节如下:

  1. coroasm 环境说明
  2. coro句柄结构

这篇文章大抵是翻译的 x64_cheatsheet 这一篇教案, 添加了一些自己的注解,并修复了代码的错误之处

要求对于 汇编指令、C的函数栈有一定的了解

1. X64 Register

8-byte register Bytes 0-3 Bytes 0-1 Byte 0
%rax %eax %ax %al
%rcx %ecx %cx %cl
%rdx %edx %dx %dl
%rbx %ebx %bx %bl
%rsi %esi %si %sil
%rdi %edi %di %dil
%rsp %esp %sp %spl
%rbp %ebp %bp %bpl
%r8 %r8d %r8w %r8b
%r9 %r9d %r9w %r9b
%r10 %r10d %r10w %r10b
%r11 %r11d %r11w %r11b
%r12 %r12d %r12w %r12b
%r13 %r13d %r13w %r13b
%r14 %r14d %r14w %r14b
%r15 %r15d %r15w %r15b

2. 操作符

  • Imm refers to a constant value, e.g. 0x8048d8e or 48,
  • Ex refers to a register, e.g. %rax,
  • R[Ex] refers to the value stored in register Ex, and
  • M[x] refers to the value stored at memory address x.

3. X64 指令

操作符后缀

  • “byte” refers to a one-byte integer (suffix b),
  • “word” refers to a two-byte integer (suffix w),
  • “doubleword” refers to a four-byte integer (suffix l), and
  • “quadword” refers to an eight-byte value (suffix q).

大多数指令(例如 mov)使用后缀来显示操作数的大小。例如,将 quadword 从 %rax 移动到 %rbx 形为 movq %rax, %rbx。有些指令(例如 ret)不使用后缀,因为没有必要。其他的,例如 movsmovz 将使用两个后缀,意为它们将第一个后缀类型的操作数转换为第二个后缀的类型。因此,将 %al 中的字节转换为 %ebx 中具有零扩展名的双字的汇编将是 movzbl %al, %ebx

movzbl: move byte to doubleword, with zero extended

在以下表格中,非注明时,指令只有单个后缀

3.1 数据移动

Instruction Description Page #
Instructions with one suffix
mov S, D Move source to destination 171
push S Push source onto stack 171
pop D Pop top of stack into destination 171
Instructions with two suffixes
mov S, D Move byte to word (sign extended) 171
push S Move byte to word (zero extended) 171
Instructions with no suffixes
cwtl Convert word in %ax to doubleword in %eax (sign-extended) 182
cltq Convert doubleword in %eax to quadword in %rax (sign-extended) 182
cqto Convert quadword in %rax to octoword in %rdx:%rax 182

3.2 算术操作

非注明时,算术操作均仅包含单个操作数说明符

3.2.1 Unary Operations

Instruction Description Page #
inc D Increment by 1 178
dec D Decrement by 1 178
neg D Arithmetic negation 178
not D Bitwise complement 178

3.2.2 Binary Operations

Instruction Description Page #
leaq S, D Load effective address of source into destination 178
add S, D Add source to destination 178
sub S, D Subtract source from destination 178
imul S, D Multiply destination by source 178
xor S, D Bitwise XOR destination by source 178
or S, D Bitwise OR destination by source 178
and S, D Bitwise AND destination by source 178

3.2.3 Shift Operations

Instruction Description Page #
sal/shl k , D Left shift destination by k bits 179
sar k, D Arithmetic right shift destination by k bits 179
shr k, D Logical right shift destination by k bits 179

3.2.4 Special Arithmetic Operations

Instruction Description Page #
imulq S Signed full multiply of %rax by S
Result stored in %rdx:%rax
182
mulq S Unsigned full multiply of %rax by S
Result stored in %rdx:%rax
182
idivq S Signed divide %rdx:%rax by S
Quotient stored in %rax
Remainder stored in %rdx
182
divq S Unsigned divide %rdx:%rax by S
Quotient stored in %rax
Remainder stored in %rdx
182

3.3 Comparison and Test Instructions

比较指令也有单个后缀

Instruction Description Page #
cmp S2, S1 Set condition codes according to S1 - S2 185
test S2, S1 Set condition codes according to S1 & S2 185

3.4 Accessing Condition Codes

Condition Codes: http://web.cse.ohio-state.edu/~reeves.92/CSE2421au12/SlidesDay41.pdf
以下指令均无后缀

特殊的suffix

  • n: not
  • g: greater
  • l: less
  • e: equal
  • a: above
  • b: below

    setnle : set if not less or equal then, this is same as setg

3.4.1 Conditional Set Instructions

Instruction Description Page #
sete / setz D Set if equal/zero ZF 187
setne / setnz D Set if not equal/nonzero ~ZF 187
sets D Set if negative SF 187
setns D Set if nonnegative ~SF 187
setg / setnle D Set if greater (signed) (SF^0F)&ZF 187
setge / setnl D Set if greater or equal (signed) ~(SF^0F) 187
setl / setnge D Set if less (signed) SF^0F 187
setle / setng D Set if less or equal (SF^OF) ZF
seta / setnbe D Set if above (unsigned) CF&ZF 187
setae / setnb D Set if above or equal (unsigned) ~CF 187
setb / setnae D Set if below (unsigned) CF 187
setbe / setna D Set if below or equal (unsigned) CF ZF

3.4.2 Jump Instructions

Instruction Description Page #
jmp Label Jump to label 189
jmp *Operand Jump to specified location 189
je / jz Label Jump if equal/zero ZF 189
jne / jnz Label Jump if not equal/nonzero ~ZF 189
js Label Jump if negative SF 189
jns Label Jump if nonnegative ~SF 189
jg / jnle Label Jump if greater (signed) (SF^0F)&ZF 189
jge / jnl Label Jump if greater or equal (signed) ~(SF^0F) 189
jl / jnge Label Jump if less (signed) SF^0F 189
jle / jng Label Jump if less or equal (SF^OF) ZF
ja / jnbe Label Jump if above (unsigned) CF&ZF 189
jae / jnb Label Jump if above or equal (unsigned) ~CF 189
jb / jnae Label Jump if below (unsigned) CF 189
jbe / jna Label Jump if below or equal (unsigned) CF ZF

3.4.3 Conditional Move Instructions

条件数据移动指令 均为后缀,但隐式要求两个操作数有相同的大小

Instruction Description Page #
cmove / cmovz S, D Move if equal/zero ZF 206
cmovne / cmovnz S, D Move if not equal/nonzero ~ZF 206
cmovs S, D Move if negative SF 206
cmovns S, D Move if nonnegative ~SF 206
cmovg / cmovnle S, D Move if greater (signed) (SF^0F)&ZF 206
cmovge / cmovnl S, D Move if greater or equal (signed) ~(SF^0F) 206
cmovl / cmovnge S, D Move if less (signed) SF^0F 206
cmovle / cmovng S, D Move if less or equal (SF^OF) ZF
cmova / cmovnbe S, D Move if above (unsigned) CF&ZF 206
cmovae / cmovnb S, D Move if above or equal (unsigned) ~CF 206
cmovb / cmovnae S, D Move if below (unsigned) CF 206
cmovbe / cmovna S, D Move if below or equal (unsigned) CF ZF

3.5 Procedure Call Instruction

调用指令无后缀

Instruction Description Page #
call Label Push return address and jump to label 221
call *Operand Push return address and jump to specified location 221
leave Set %rsp to %rbp, then pop top of stack into %rbp 221
ret Pop return address from stack and jump there 221

4. Coding Practices

4.1 Commenting

建议在合适的位置添加注释

4.2 Arrays

数组作为连续的数据块存储在内存中。通常,数组变量等价于指向内存中数组第一个元素的指针。要访问给定的数组元素,请将索引值乘以元素大小并添加到数组指针。例如,如果 arr 是一个整数数组,则语句:

1
arr[i] = 3;

可以被解释为如下x86-64汇编 (假设数组地址与索引值分别存储在 %rax,%rcx):

1
movq $3, (%rax, %rcx, 8)

4.3 Register Usage

x86-64 中有 16 个 64 位寄存器:%rax、%rbx、%rcx、%rdx、%rdi、%rsi、%rbp、%rsp 和 %r8-r15。其中,%rax、%rcx、%rdx、%rdi、%rsi、%rsp 和 %r8-r11 被视为调用者保存寄存器(caller-save registers),这意味着它们不一定在函数调用之间保存。按照约定,%rax 用于存储函数的返回值(如果存在且长度不超过 64 位)。 (较大的返回类型(如结构)是使用栈返回的)。寄存器 %rbx、%rbp 和 %r12-r15 是被调用者保存寄存器(callee-save registers),这意味着它们在函数调用之间保存。寄存器%rsp用作栈指针,指向栈中最顶层元素的指针。

此外,%rdi、%rsi、%rdx、%rcx、%r8 和 %r9 用于将前六个整数或指针参数传递给被调用的函数。附加参数(或大参数,例如按值传递的结构)在栈上传递。

在 32 位 x86 中,基指针(以前是 %ebp,现在是 %rbp)用于跟踪当前栈帧的基地址,被调用函数将在更新基指针之前保存其调用者的基指针到它自己的栈帧。随着 64 位体系结构的出现,这种情况基本上已被消除,除了一些特殊情况,即编译器无法提前确定需要为特定函数分配多少栈空间(请参阅动态栈分配)。

4.4 Stack Organization and Function Calls

4.4.1 Calling a Function

要调用函数,程序应将前六个整数或指针参数放入寄存器 %rdi、%rsi、%rdx、%rcx、%r8 和 %r9 中;后续参数(或大于 64 位的参数)应压入栈,第一个参数位于最上面。然后程序应该执行调用指令,该指令会将返回地址压入栈并跳转到指定函数的开头。

事实上这里的参数压栈跟函数调用约定有关系

Example:

1
2
3
4
# Call foo(1, 15)
movq $1, %rdi # Move 1 into %rdi
Movq $15, %rsi # Move 15 into %rsi
call foo # Push return address and jump to label foo

函数若有返回值,约定存储在 %rax(如果大于8字节,则压栈返回)

4.4.2 Writing a Function

x64 程序使用称为栈的内存区域来支持函数调用。顾名思义,该区域被组织为栈数据结构,栈的“顶部”向较低的内存地址增长。对于每个函数调用,都会在栈上创建新空间来存储局部变量和其他数据。这称为栈帧。为此,您需要在每个函数的开头和结尾编写一些代码来创建和销毁栈帧。
Setting Up: 当执行调用指令时,后续指令的地址作为返回地址被压入栈,并且控制权传递到指定的函数。如果函数要使用任何被调用者保存寄存器(%rbx、%rbp 或 %r12-r15),则应将每个寄存器的当前值压入栈,以便在最后恢复。例如:

1
2
3
Pushq %rbx
pushq %r12
pushq %r13

最后,可以在栈上为局部变量分配额外的空间。虽然可以根据需要在函数体中在栈上腾出空间,但在函数开始时一次性分配该空间通常会更有效。这可以使用调用subq $N, %rsp来完成,其中 N 是被调用者栈帧的大小。
For example:

1
subq $0x18, %rsp # Allocate 24 bytes of space on the stack

这被称作 function prologue.

使用栈帧:一旦设置了栈帧,就可以使用它来存储和访问局部变量:

  • 无法容纳在寄存器中的参数(例如结构体)将在调用指令之前被推送到栈上,并且可以相对于 %rsp 进行访问。请记住,以这种方式引用参数时,您需要考虑栈帧的大小。
  • 如果函数有超过六个整数或指针参数,它们也将被压入栈。
  • 对于任何栈参数,编号较小的参数将更接近栈指针。也就是说,在适用时,参数按从右到左的顺序推送。
  • 当从 %rsp 中减去一些量时,局部变量将存储在函数序言中分配的空间中。这些的组织由程序员决定。

Cleaning Up: 函数体完成并将返回值(如果有)放入 %rax 后,函数必须将控制权返回给调用者,将栈恢复到调用时的状态。首先,被调用者通过将相同的值添加到栈指针来释放其分配的栈空间:

1
2
3
4
5
6
7
8
9
addq $0x18, %rsp # Give back 24 bytes of stack space

# Then, it pops off the registers it saved earlier
popq %r13 # Remember that the stack is FILO!
popq %r12
popq %rbx

# Finally, the program should return to the call site, using the ret instruction:
ret

Summary: 合起来,一个函数的汇编应该如下:

1
2
3
4
5
6
7
8
9
10
11
foo:
pushq %rbx # Save registers, if needed
pushq %r12
pushq %r13
subq $0x18, %rsp # Allocate stack space
# Function body
addq $0x18, %rsp # Deallocate stack space
popq %r13 # Restore registers
popq %r12
popq %rbx
ret # Pop return address and return control to caller

4.4.3 Dynamic stack allocation

你可能会发现,为函数提供静态数量的栈空间并不能完全减少它。在这种情况下,我们需要借用 32 位 x86 的传统,将栈帧的基址保存到基址指针寄存器中。由于 %rbp 是一个被调用者保存寄存器,因此在更改它之前需要保存它。
如此,函数 prologue 应该如下开始:

1
2
pushq %rbp
movq %rsp, %rbp

承上的,epilogue 应该放置如下代码在 ret之前:

1
2
movq %rbp, %rsp
popq %rbp

这也可以通过一条称为leave的指令来完成。epilogue 确保无论您对函数体中的栈指针执行什么操作,返回时总会将其返回到正确的位置。请注意,这意味着您不再需要在尾声中添加栈指针。

这是一个在执行期间分配 8-248 字节随机栈空间的函数示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pushq %rbp # Use base pointer
movq %rsp, %rbp
pushq %rbx # Save registers
pushq %r12
subq $0x18, %rsp # Allocate some stack space
...
call rand # Get random number
andq $0xF8, %rax # Make sure the value is 8-248 bytes and
# aligned on 8 bytes
subq %rax, %rsp # Allocate space
...
movq (%rbp), %r12 # Restore registers from base of frame
movq 0x8(%rbp), %rbx
movq %rbp, %rsp # Reset stack pointer and restore base
# pointer
popq %rbp
ret

此处恢复r12 等寄存器的代码有错误,请见 动态栈帧分配

这种行为可以通过调用像 alloca 这样的伪函数从 C 代码中访问,它根据其参数分配栈空间。

alloca: The alloca() function allocates size bytes of space in the stack frame of the caller. This temporary space is automatically freed when the function that called alloca() returns to its caller.

5. 注

动态栈帧分配

在gcc上可用的cpp嵌入式汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
extern "C" int func(int x);
asm(R"(
.globl func
func:

# prologue
pushq %rbp
movq %rsp,%rbp # save stack pointer
pushq %rbx # registers will be used

# epilogue
movq -0x08(%rbp), %rbx
movq %rbp, %rsp
popq %rbp
movl $0x10, %eax # return 16
ret

)");

引用

https://cs.brown.edu/courses/cs033/docs/guides/x64_cheatsheet.pdf
https://cons.mit.edu/fa17/x86-64-architecture-guide.html

使用libuchardet识别编码,再使用iconv转换源编码到utf-8

以下点需要注意:

  • 获取源的编码是重中之重,iconv本身不会有什么问题
  • 受输入源的限制,比如输入太少,特征不够,uchardet并不总是能识别到源的编码,或者是会认为是子集编码(GB18030之于GBK)。

这是在linux下测试的,win端编译iconv应该会麻烦一点,需要MYSYS环境或者github上找win工程版

1
2
3
4
5
wget https://ftp.gnu.org/gnu/libiconv/libiconv-1.17.tar.gz
tar -xvf libiconv-1.17.tar.gz
cd libiconv-1.17
./configure --prefix=`pwd`/install --enable-static
make install -j > make.log

头文件在 install 目录
编译结果静态库的路径有点奇怪,find -name libiconv.a 应该可以帮助你找到它,或者查看make.log

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
#include <fstream>
#include <iconv.h>
#include <iostream>
#include <sstream>
#include <stdio.h>
#include <string>
#include <string.h>
#include <uchardet/uchardet.h>
#include </home/lull/code/temp/libiconv-1.17/install/include/iconv.h>
#define LOG printf

static std::string require_utf8(const char* filename)
{
std::ifstream in(filename);
std::stringstream ss;
in.seekg(0, std::ios_base::end);
size_t filesize = in.tellg(), outsize = 2 * filesize;
in.seekg(0, std::ios_base::beg);

std::string inbuf, outbuf;
inbuf.resize(filesize);
outbuf.resize(outsize);
auto pp =(char*)inbuf.data();
in.read(pp, filesize);

auto uc_checker = uchardet_new();
int result = uchardet_handle_data(uc_checker, inbuf.data(), inbuf.size());
uchardet_data_end(uc_checker);
const char* charset = uchardet_get_charset(uc_checker);
LOG("source got %s\n", charset);
// iconv_t iconver = iconv_open("UTF-8", charset);
iconv_t iconver = iconv_open("utf-8", charset);

char* _pindata = (char*)inbuf.data();
char* _poutdata = (char*)outbuf.data();
int bytes = iconv(iconver, &_pindata, &filesize, &_poutdata, &outsize);
if (bytes)
{
return inbuf;
}
else
{
int outLen = strlen(outbuf.data());
outbuf.resize(outLen);
return outbuf;
}
}

int main()
{
auto utf8str = require_utf8("gbk.log");
#ifdef _WIN32
// change terminal out codepage to utf-8
system("chcp 65001");
#endif
LOG("result:%s", utf8str.c_str());
return 0;
}