第五讲 跳转、比较、条件跳转

程序员小x大约 22 分钟汇编语言

第五讲 跳转、比较、条件跳转

汇编语言没有专用的循环结构(如 fordowhile 等)。它只有下面这些特性:

  • 分支(也称为跳转、goto语句等): 跳转到程序中的新位置。
  • 比较: 比较两个操作数,然后适当地设置标志寄存器。只有一条比较指令,它执行所有可能的比较(等于、小于、等于零等)
  • 条件跳转:根据标志寄存器的状态(由比较操作先前设置)进行跳转或正常继续执行下一条指令。
  • 条件移动:根据标志寄存器的状态进行移动或不移动。

后面会学习到函数调用函数返回,它只是上述几种类型的特殊形式。

汇编语言程序的结构

汇编语言程序的结构与高级语言(如C/C++)有着根本的不同。每一条指令在汇编语言中都对应着一条CPU操作。相比之下,在C/C++中,一条语句可能在编译过程中生成多个操作。这意味着汇编语言无法像C/C++那样拥有“条件语句”或“循环语句”;在C/C++中,这些都是复合语句,内部包含其他语句。这必然意味着if-else或while循环生成了多于一条的CPU操作。因此,在汇编语言中,循环和条件的工作方式有很大的不同。

汇编语言程序最终只是一系列指令。就是这样简单。它没有真正的区分不同的函数,也没有区分循环或if-else的“主体”与所在函数的其余部分。程序只是一大堆指令,因此我们需要对其进行一些结构化处理。通常的编程语言结构,如函数、条件语句、循环,都是我们自己去构建的。

汇编语言程序中的每条指令都有一个地址,即程序最终运行时它所在的内存位置。添加标签告诉汇编器该地址(标签后紧跟的指令的地址)很重要,重要到足以被保存并赋予一个名字。因此,当我们写下 _start: 时,_start 的“值”就是紧随其后的第一条指令的地址。对于局部标签(以 . 开头的标签)也是如此。

汇编语言程序中的正常控制流非常简单:每条指令按顺序执行,从第一条到最后一条。CPU总是知道程序中下一条要执行的指令:就是紧接在当前指令后面的那条指令。

汇编语言支持的唯一其他控制流类型是,跳转到一个地址(由CPU实现为改变rip,指令指针寄存器的值)。我们所有现有的控制流结构(if-elseswitch-casewhiledo-while)都必须被翻译成这种基本的概念,即要么跳过程序中的一些指令,要么向后跳转,以便一些程序中的地址被多次提供给CPU执行指令。

跳转

跳转通常是指jmp指令,它可以使程序跳转到新位置来工作。jmp指令会通过修改rip从而加载指定位置的代码进行执行。我们只需要提供要跳转的目标就可以实现跳转:

jmp target

跳转标签

跳转的目标必须是一个标签。标签由标识符后跟冒号组成:

Target:
    ...

除此以外,还有一种标签是本地标签本地标签是名称以句号开头的标签。本地标签的全名需要添加靠的最近的非本地标签的名称。例如:

my_function:
  ...
  .begin_loop:    ; Full name: my_function.begin_loop

这允许我们拥有具有相同名称的多个标签,只要它们位于不同的函数/代码块中。

标签只是一个地址,即下一条指令的地址(或数据,如果在 .data 部分中使用)。

**jmp**指令

要跳转到一个标签,请使用 jmp 指令:

jmp target

请注意,标签的值只是它在程序中的地址。

mov rax, Target 
jmp rax

可以在寄存器中存储标签, 跳转到寄存器等。这有时称为计算跳转。例如,我们可以将一组标签存储在数组中(在 .data 部分),然后使用数组索引来确定要跳转到哪个标签。稍后将使用该技术来实现 switch-case 结构。

比较

cmp指令

有两种比较指令,其中 cmp 是最为直接的比较, 它需要两个操作数,并且两个操作数的大小必须相同。第一个操作数不能是立即数,但第二个操作数可以。 其中一个操作数可以位于内存中,但不能同时位于内存中。

cmp op1, op2

cmp指令会在内部执行 op1 - op2 ,并且丢弃结果,但会更新标志寄存器。例如,如果 op1 - op2 == 0,则零标志 ZF 将被设置,因此置零标志告诉我们原始操作数是相等的。类似地,如果 op1 > op2,减法会设置进位标志。

条件

可以使用标志的各种组合来确定 sub a, b 的两个操作数之间的关系:

  • 如果 a == b 则结果将为 0,这将设置 ZF = 1。因此我们可以通过查看零标志来检测相等性。
  • 如果 a != b, 那么结果将是非零的,因此 ZF == 0
  • 如果 a > b, 无符号, 那么结果是非零的, 并且不需要额外的进位/借位,因此 ZF == 0 且 CF == 0。
  • 如果 a >= b, a和b都是无符号整数,那么不需要额外的进位/借位(结果可能为零也可能不是),因此 CF == 0
  • 如果 a < b,a和b都是无符号整数,与a >= b相反,因此 CF == 1(需要额外借位)。
  • 如果,a <= b,a和b都是无符号整数,与a > b相反,因此 ZF == 1 或 ```CF == 1``。
  • 如果 a > b,a和b都是有符号整数,那么事情就更有趣了:我们知道结果不会为 0,因此 ZF == 0,但结果的其余部分取决于溢出标志和符号标志:
    • 如果ab具有相同的符号,则OF == 0(不可能溢出)。如果 a > b 且两者均为正,则结果将为正 (SF == 0)。如果两者均为负数,则结果也将为正数(例如,-2 > -10、-2 - -10 = +8)。所以在这种情况下我们有 SF == OF
    • 如果 a 和 b 的符号不同,则可能发生溢出。如果 a > -b 那么我们正在做 a - -b = a + b:
    • 如果a + b没有溢出,则符号为正,所以SF == OF == 0
    • 如果 a + b 确实溢出,则符号为负,但 OF == 1,所以我们有 OF == SF == 1 不管怎样,我们再次得到 SF == OF。 因此,a > b 有符号的最终条件是 ZF == 0 且 SF == OF
  • 如果 a >= b,有符号,那么我们只需忽略零标志:SF == OF
  • a < b`` 与a >= b相反 ,因此 SF != OF```。
  • a <= ba > b 相反,因此 ZF == 1SF != OF

这些条件代码中的每一个都将在稍后的条件跳转指令中使用。对于有符号比较,我们通常使用术语“小于”和“大于”;对于无符号比较,我们说“低于”和“高于”。

cmps*指令(内存与内存的比较)

cmp 指令无法直接比较内存中的两个操作数。 但是cmps* 系列指令可以比较内存中的两个操作数,第一个操作数位于 [rsi],第二个操作数位于 [rdi]

cmps*的指令如下所示:

指令描述
cmpsb比较 byte [rsi]byte [rdi]
cmpsw比较 word [rsi]word [rdi]
cmpsd比较 dword [rsi]dword [rdi]
cmpsq比较 qword [rsi]qword [rdi]

cmps* 指令不带任何操作数;他们总是使用 rsirdi

test指令

cmp 执行减法并更新与 sub 相同的标志,而 test 执行二进制 AND 并仅更新 SF、ZF 和 PF 标志, CF 和 OF 标志被清除。这意味着test不能用于确定依赖于这些标志的任何条件(排序比较,例如大于、小于、高于、低于)或相等。因为它使用 AND 而不是减法,所以test的用途更加有限:

  • 判断寄存器是否等于0:
 test reg, reg
 jz target          ; or je target, jump if ZF == 1

如果一个数和自身相与,结果还是自身。因此结果等于0的唯一可能是 reg = 0JEJZ仅在ZF == 1时跳跃。

  • test reg, reg 还可用于确定寄存器的符号:如果 SF == 1 则 reg 为负数。因此,我们可以做:
test reg, reg
js target

如果 reg < 0,则跳转到目标。(如果 reg 无符号,则在设置高位时将跳转。)

  • 类似地,如果 reg <= 0,测试 reg、reg 和 jle 将跳转,尽管要弄清楚为什么这样做需要做一些工作:

    • 如果 ZF == 1 或 SF != OF,则 jle 跳转
    • 测试总是设置 OF = 0,所以这实际上是 ZF == 1 或 SF == 1
    • ZF == 1 是上面用于 reg == 0 的条件
    • SF == 1 是上面用于 reg < 0 的条件
    • 所以 ZF == 1 或 SF == 1 相当于 reg <= 0 当然,只有当 reg 是有符号值时这才有意义
  • test reg, 00000010b 可用于测试寄存器中是否设置了特定位(或位组合)。如果 AND 的结果为 0,则该位未被设置,且 ZF == 1;如果该位被设置,则 ZF == 0。所以我们可以这样做

    test reg, 00000010b
    jnz target              ; jump if bit 2 is set
    

    这可能是test的最主要的用途。

test 的操作数受到一些限制:

  • 第二个操作数必须是寄存器或立即数,而不可以是内存。
  • 第一个操作数可以是寄存器或内存。
  • 两者尺寸必须相同

test指令不会修改两个操作数;仅更改标志寄存器

其他指令

请记住,除了cmptest以外,许多其他指令都会设置标志寄存器,

例如,假设您想要递减 rcx,然后跳转到某个位置,只要它不等于 0。这可以通过以下方式完成:

dec rcx
jnz target

如果结果为 0,dec 将设置 ZF,因此无需使用 cmptest

另一个例子是,如果我们需要执行减法 rax -= rbx,然后根据 rax 是否为 0决定是否跳转。如果这样做会很浪费:

sub rax, rbx
cmp rax, 0
jz label

我们可以直接这样做:

sub rax, rbx
jz label

条件跳转的指令

条件分支指令检查标志寄存器来决定跳转到目标或者不跳转到目标。这些通常称为 jcc,其中 cc 是条件代码(conditional code), jcc指令如下表所示:

|操作|描述|标志位的状态| |je|当op1 == op2时跳转| ZF == 1| |jne|当op1 != op2时跳转| ZF == 0| |jl|当op1 < op2时跳转, 针对有符号的数据比较|SF != OF| |jle|当op1 <= op2时跳转, 针对有符号的数据比较|ZF ==1 or SF != OF| |jg|当op1 > op2时进行跳转, 针对有符号的数据比较|ZF == 0 && SF ==OF| |jge|当op1 >= op2时进行跳转, 针对有符号的数据比较|SF ==OF| |jb|当op1 < op2时进行跳转, 针对无符号的数据比较|CF == 1| |jbe|当op1 <= op2时进行跳转, 针对无符号的数据比较|CF == 1 or ZF == 1| |ja|当op1 > op2时进行跳转, 针对无符号的数据比较|CF ==0 && ZF == 0| |jae|当op1 >= op2时进行跳转, 针对无符号的数据比较|CF == 0|

对于无符号数据的比较,"a"是"above"的缩写, "b"是"below"的缩写。 对于有符号的比较, "g"是"greater"的缩写, "l"是"less"的缩写。"e"是"equal"的缩写。

C/C++ 没有反向比较, 例如 !<, !>=,汇编支持这个语法。例如 jnl(不小于)就是 jge 的同义词。

下面是一些上述符号的同义词

|操作|描述| |jna|当op1 <= op2时进行跳转, 针对无符号的数据比较| |jnae|当op1 < op2时进行跳转, 针对无符号的数据比较| |jnb|当op1 >= op2时进行跳转, 针对无符号的数据比较| |jnbe|当op1 > op2时进行跳转, 针对无符号的数据比较| |jng|当op1 <= op2时进行跳转, 针对有符号的数据比较| |jnge|当op1 < op2时进行跳转, 针对有符号的数据比较| |jnl|当op1 >= op2时进行跳转, 针对有符号的数据比较| |jnle|当op1 > op2时进行跳转, 针对有符号的数据比较|

这些只是上述指令的别名(例如,jnajbe 的别名)。

有一组跳转通过检查 rcx 寄存器来模拟循环操作:

操作描述
jcxzcx == 0时进行跳转
jecxzecx == 0时进行跳转
jrcxzrcx == 0时进行跳转

请注意,如果 rcx 等于 0,则这些跳转,而如果 rcx 不等于 0,则循环跳转。

最后,有一组直接引用标志寄存器名称的条件跳转:

操作描述
jcCF == 1时跳转
jncCF == 0时跳转
jzZF == 1时跳转
jnzZF == 0时跳转
joOF == 1时跳转
jnoOF == 0时跳转
jsSF == 1时跳转
jnsSF == 0时跳转
jzZF == 1时跳转
jnzZF == 0时跳转
jpPF == 1时跳转
jpoPF == 0时跳转
jpePF == 1时跳转
jnpPF == 0时跳转

注意JNP == JPOJP =JPE

机器码指令描述
7B cbJNPJump short if not parity (PF=0).
7A cbJPJump short if parity (PF=1).
7A cbJPEJump short if parity even (PF=1).
7B cbJPOJump short if parity odd (PF=0).

例如,假设我们要实现以下代码:

if(rcx == 0)
    rax = rbx;

使用条件跳转,我们可以这样做:

cmp rcx, 0
jne NotZero
mov rax, rbx
NotZero:

跳转目标

普通的jmp指令可以跳转到任意地址。条件跳转存储的是跳转目标距离当前指令的偏移量。偏移量是有符号 8 位或 32 位数值。在汇编语言中,我们编写一个标签,汇编器会计算相应的偏移量,写入指令中。

条件跳转到计算目标

通过无条件跳转,可以很容易地跳转到寄存器定义的目标:

target:
...
mov rax, target
jmp rax

因为rax寄存器的值就是要跳转到的地址。由于条件跳转不使用绝对地址,而是使用当前地址的偏移量,因此计算条件跳转需要更多技巧。

最简单的方法是使条件跳转到固定目标,其中目标是到计算地址的普通跳转:

jcc my_jmp_target

  ⋮

my_jmp_target:  jmp rax

此方法比理想慢一些,因为它涉及两次跳跃。更快的方法是计算最终的 jcc 指令和各个跳转目标之间的距离,然后将这些距离存储到某个寄存器中。由于这些距离在组装时是固定的,因此在运行时计算它们的效率很低。一般的策略是给条件跳转指令本身加上标签,这样我们就可以访问它的地址:

target1:

  ⋮
                mov rax, computed_jump - target1  ; Pick target to jump to
computed_jump:  jcc rax                           ; Jump 

  ⋮

target2:

mov 当然是条件结构的一部分,它要么:

mov rax, computed_jump - target1

或者

mov rax, computed_jump - target2

取决于某些条件。我们还可以存储一个compated_jump - target1、compated_jump - target2等偏移量的数组,然后对其进行索引.

查询了intel指令集open in new window volume 2中关于jcc的部分,jcc后面不可以带register,因此课程中这里讲解的似乎不太正确。

复合条件

我们如何检查复合条件,例如 rbx >= 10 和 rbx < 100,并在复合条件为真时执行跳转?

  • 一种方法是执行多步跳转
    cmp rbx, 10
    jge .step1
    jmp .else
    
    .rax_ge_0:
    cmp rbx, 100
    jnge .else
    
        ; rbx >= 0 and rbx < 100
    
    .else:
    
        ; condition failed.
    

除最后一个条件外,每个条件都需要自己的 cmp 和条件跳转。 (因为 cmp 在进行比较之前重置标志,所以您无法“组合”多个比较。)

这实际上相当于转换

 if(rbx >= 10 and rbx <= 100) {
  ...
 }

into

 if(rbx >= 10) {
     if(rbx <= 100) {
      ...
     }
 }
  • set** 检查特定条件标志(或标志组合)并将(字节)寄存器/内存设置为 1 或 0。然后可以使用正常和/或/非按位操作以及 z、nz 条件将它们组合起来可用于检查假/真。例如。,
 cmp rbx, 10
 setge al
 cmp rbx, 100
 setl  ah
 and al, ah      ; Sets the zero flag if al && ah == 0
 jz .outside

    ; Inside

 .outside:

set** 支持与条件跳转相同的一组条件代码。

  • lahf 指令可用于将 CF、ZF、SF 和 PF 标志的值保存到 ah 寄存器中以供以后操作。因为这不包括 OF,所以带符号的比较不能与此方法一起使用。

  • 上面的例子这样的范围检查实际上有一个使用减法的简单版本

 sub rbx, 10
 cmp rbx, 100 - 10
 jae .outside
    ; Inside the range

 .outside:
    ; Outside the range 

这是有效的,因为如果 rbx < 10 减法将回绕到一个值,因此值 < 10 和值 >= 100 将跳转到 .outside。假设 rbx 是无符号的,这是可行的。

跳转优化

条件跳转代价很大!(无条件跳转比正常的顺序控制流更昂贵,但不如条件跳转代价那么大)。处理器在检查标志寄存器之前不知道将采用什么指令,这意味着它执行的许多优化必须被延迟。

优化跳转的最佳方法是尽量减少它们的使用:尝试尽可能保持控制流的顺序。除此之外,尝试

  • 尽量使用距离短跳转, 跳转距离在 +-127 字节内
  • 当一个条件语句大多数时候为真和假的场景不平衡时,适合使用条件跳转。因为这个时候处理器会进行分支预测,处理器会的使得少数场景进行跳转,而多数场景不进行跳转。 例如,在循环中,循环条件大多数时候为 true,只有在最后结束才为 false。处理器将学习这种行为并猜测循环将重复,因此大多数循环跳转都会很快。只有最后的跳转(跳出循环)才会很慢,因为这就是预测失败的地方。
  • 通过条件移动和setcc指令来完全避免跳转跳转

setcc和bool变量

有时,在 C/C++ 中,我们依赖 bool → int 的隐式转换来避免编写 if/else。例如,要计算数组中负值的数量,我们可以这样做:

int c = 0;
for(int* p = arr; p < arr + size)
   c += (*p < 0);

这是有效的,因为 bool值 true 转换为 1(从而成为 c += 1),而 false 转换为 0(成为 c += 0)。该代码实际上比等效代码更快:

int c = 0;
for(int* p = arr; p < arr + size)
   if(*p > 0)
      ++c;

因为计算条件分支对于 CPU 来说速度较慢。为了实现上述版本,我们可以使用 setcc指令,如果满足条件代码 cc,则将给定(字节)寄存器设置为 1,如果不满足,则将给定(字节)寄存器设置为 0。例如,仅当 rbx > 0 时才增加 rax,我们可以这样做:

mov rcx, 0

cmp rbx, 0
seta cl      ; Set cl = 1 if rbx > 0
add rax, rcx

转换 C/C++ 结构

if-else 链

经典的 C/C++ if-else 结构如下所示:

if(condition1) {
  ... // body1
}
else if(condition2) {
  ... // body2
}
...
else {
  ... // else body
}

在汇编中没有直接if-else的语句。我们需要使用比较条件跳转无条件跳转来构建。

  • 每一个if语句都需要cmp或者test指令(如果if表达式比较复杂,不是简单的数值比较,那么可能需要多个cmp或者test)。如果条件为假则进行条件跳转。跳转目标是链中的下一个 if。
  • 每个 if 的主体在最后的 else 结束后以无条件跳转到标签结束。
  • else 的主体不需要跳转,因为它直接跳转到下面的代码。
cmp ...
jncc .elseif1 
  ; body 1

  jmp end_else

.elseif1:
cmp ...
jncc .elseif2
  ; body2

  jmp end_else

... ; other else-if comparisons and bodies

.else: 

  ; else body

.end_else:

...

当然,您应该尝试使用更具描述性的标签名称!

嵌套的 if-else

嵌套的 if-else,例如:

if(rax == rbx) {
  if(rbx < rcx) {
    ...
  }
}

上述的代码可以翻译成下面的汇编代码:

cmp rax, rbx      ; Or whatever you need for the outer condition
jne .end          ; Note: jump if NOT equal
cmp rbx, rcx      
jge .end

...               ; Actual body 

.end
...               ; Rest of program

我们依次测试每个条件,如果不满足条件,则跳转到嵌套 if 主体之后的标签。

do-while循环

在之前,我们已经学习过使用loop指令实现 do-while 循环的方式,只要您使用 rcx 作为循环变量,在循环中进行递减,当 rcx == 0 时循环结束。

通过条件跳转,我们可以构建一个更通用的 do-while 循环,在循坏开始时需要设置一个标签, 在循环结束时测试循环条件。

.do                 ; do {

  ...               ;   Loop body

  cmp rax, rbx      
  je .do            ; } while(rax == rbx);

while 循环

实现 while 循环需要在循环开始时测试循环条件,如果失败则可能跳到循环末尾。

因此,我们需要在循环的开头和循环的结尾都有一个标签。(循环开头的标签用于满足循环条件时进入循环, 循环结尾的标签用于循环条件不成立时跳出循环):

.while:         ; while(rax != rbx) {
  cmp rax, rbx
  je .end_whle

  ...           ;   Loop body

  jmp .while
.end_while:     ; }

for 循环只是一种特殊的 while 循环,例如

for(rax = 0; rax < 100; ++rax) {
  ...
}

将会编译为:

  xor rax, rax      ; rax = 0
.for:     
  cmp rax, 100
  jge .end_for

  ...               ; Loop body

  inc rax
  jmp .for
.end_for:

breakcontinue

break 等价于跳转到循环结束后的位置, 而continue 等价于跳转到循环开头的位置。

下面是一个常见的break语句。

if(condition)
  break; // Or continue

其可以通过条件跳转到循环结尾/开头来实现break/continue;无需模拟整个 if 结构。

.loop_:     
    jmp .end_loop_ ; break
    jmp .loop_     ; continue
.end_loop_:

switch-case语句

if-else 不同,switch-case 没有对汇编的单一转换。根据 case 标签的数量及其值,编译器可能会将 switch-case 转换为如上所述的 if-else 链,或转换为基于表的跳转, 甚至是类似哈希表的结构。我们将研究第二种选择,构建一个跳转目标表,然后使用它来实现 switch-case

;;;; 
;;;; switch_case.s
;;;; Implementing a switch-case statement as a jump table.
;;;;

section .data

jump_table: dq  _start.case0, _start.case1, _start.case2, _start.case3

section .text

global _start
_start:

  ; Switch on rcx = 0, 1, 2, 3, default
  mov rbx, qword [jump_table + 8*rcx]
  cmp rcx, 4
  jae .default
  jmp rbx

.case0:

  ...
  jmp .end_switch

.case1:

  ...
  jmp .end_switch 

.case2:

  ...
  jmp .end_switch

.case3:

  ...
  jmp .end_switch

.default:
  ...

.end_switch
  ...

注意:

  • 在跳转表的定义中,我们必须使用.case标签的全名。如果我们只写.case0,它将引用(不存在的)标签jump_table.case0。
  • 每个case都必须以跳转到switch末尾来结束。这就是为什么每一个案件都必须以break结束! (如果省略跳跃,会发生什么?)
  • 内存操作数 qword [jump_table + 8*rcx] 使用内存查找的扩展形式,我们稍后会介绍它:可以说内存操作数比 [addr] 更通用。在这种情况下,我们使用jump_table作为查找的位移,然后将rcx乘以8,因为每个表条目都是64位(8字节)。

case 标签表的索引始终为 0, 1, 2, 3, ... 如果实际的 case 标签值与此不对应,那么我们必须以某种方式对其进行转换(编译器通常会这样做以供使用)。例如,如果我们的标签是 10、11、12、13,我们可以简单地减去 10 并将其用作我们的索引。如果标签是 10、20、30、40,我们可以除以 10 并减 1。如果标签是 3、1、2、0,我们可以对案例重新编号。

如果案例标签不适合任何模式,我们可能必须简单地循环遍历值数组才能找到正确的标签,甚至可能进行二分搜索(如果标签值集足够大)。在本例中,我们有两个数组,一个是标签值,另一个是标签目标。

小写转换为大写的函数

;;; uppercase
;;; Converts byte [rdi] from uppercase to lowercase.
;;;
uppercase:
  ; rdi = addr. of byte to convert

  cmp byte [rdi], 'a'
  jb .done
  cmp byte [rdi], 'z'
  ja .done

  sub byte [rdi], 32 

  .done
  ret

这相当于:

if(*rdi >= a)
  if(*rdi <= z)
      *rdi -= 32;

这也可以通过使用基于减法的范围测试技巧来完成,前提是我们首先将值移入寄存器

uppercase:

  mov al, byte [rdi]
  sub al, 'a'         ; Values below 'a' will overflow
  cmp al, 'z' - 'a'
  ja .done

  sub byte [rdi], 32

  .done:
  ret

附录

课程资源

原文链接:

第五讲: https://staffwww.fullcoll.edu/aclifton/cs241/lecture-branching-comparisons.htmlopen in new window

第六讲: https://staffwww.fullcoll.edu/aclifton/cs241/branching-conditions-applications.htmlopen in new window

http://ics.p.lodz.pl/~dpuchala/LowLevelProgr/open in new window

https://www.felixcloutier.com/x86/open in new window

https://www.felixcloutier.com/x86/jccopen in new window

https://www.felixcloutier.com/x86/jmpopen in new window

Loading...